Skip to content

cycloidgen v7.6.0

Choose a tag to compare

@github-actions github-actions released this 27 Aug 09:32
· 4 commits to main since this release

Numbers — none move. Every existing design analyses exactly as it did:
motor_kind defaults to none, nothing about the motor feeds back into the
geometry, and tests/test_motor.py holds that promise by comparing whole report
sections between a design with a motor stated and the same design without one.
What is new is a set of numbers that were not there before.

Added

  • The motor is a curve now, not a mounting face. The app has always known
    which NEMA frame a drive bolts to and nothing whatever about what comes out of
    the shaft — so the duty point was taken on trust: an output torque at a speed,
    worked back through the ratio and the efficiency to an input torque nobody
    then asked about. That is a complete answer to is the gearbox strong enough
    and no answer at all to will this turn, which is the question a drive is
    bought to settle. Set a torque curve under Motor in the panel and the
    analysis puts the duty point on it: what the motor has there, what is being
    asked of it, and the margin between.

    Two models, both closed form, both stated with their limits in
    core/motor.py.
    A stepper's torque follows its phase current, and the current is what the
    supply can force through the winding against its resistance, its inductive
    reactance and its own back-EMF — V^2 = (K*w_m + I*R)^2 + (w_e*L*I)^2, solved
    for I and capped at the rated current. A DC or brushless motor gets the
    straight line between Kt*V/R at stall and Kv*V at no load, with the
    continuous rating carried as a separate, much lower line.

    Eight numbers off a datasheet, and three of them are shared between the two
    kinds because the sharing is physical rather than a squeeze: both motors run
    off the same bus, both datasheets print a winding resistance, and both have a
    current they will hold all day. What is not shared is what that current
    means — a stepper is current limited everywhere, so its curve is already its
    continuous curve, and a brushless motor's is drawn at stall current with a
    continuous line two orders of magnitude below it. Sizing a gearbox on the
    first is the classic way to build one that survives the bench and not the
    robot, so the margin reported here is on the continuous line and the peak is
    carried alongside rather than instead.

    The identity that makes this cheap is worth stating: torque per amp and volts
    per radian per second are the same constant, so a stepper's electrical
    ceiling falls out of its mechanical rating with nothing else needed —
    n_ceiling = 60*V/(2*pi*K), where K = T_hold/(sqrt(2)*I_rated) because
    holding torque is measured with both phases energised. On 24 V a 0.4 Nm,
    1.7 A motor makes no torque at all past about 1380 rpm; on 12 V the same motor
    stops at 690. Halving the supply halves the top speed, and the supply is
    usually the cheapest thing in the design to change.

    It also says something people do not expect: on a fixed bus and a fixed
    current, more holding torque buys less top speed. More torque per amp is
    more volts per rad/s, so the bigger motor meets the supply sooner. Buying
    torque without buying current or volts moves the ceiling down.

  • Three checks and a reading. MOTOR_OPERATING_POINT says where on the
    curve this duty point sits — how much of the standstill torque is left here
    and how close to the ceiling it is, which is the number that catches a drive
    designed on holding torque. MOTOR_TORQUE_SHORT is the test — thin
    margin, a duty that is a burst rather than a rating, or a motor that will not
    turn the drive at all. A warning in every case, including the last: an error
    in this app means the files are wrong, and these files are right. What is
    wrong is which motor goes on the end, which is fixed by buying a different one
    as readily as by redrawing anything — the same argument MOTOR_SHAFT_MISMATCH
    has always been a warning on. Blocking the export would also have made the app
    refuse to hand over a gearbox somebody is having machined so they can go and
    find a motor for it. MOTOR_SUPPLY_VOLTAGE catches the
    one thing the winding resistance decides on its own: a bus that cannot push
    the rated current through a stationary winding, which scales the whole curve
    down from the datasheet's, standing still included.

  • A MOTOR tab, the curve with the duty point on it and the crossing where
    the motor runs out labelled, plus seven rows on the datasheet and a section in
    the PDF. Two of those rows are the ones to design against: the output torque
    this motor buys
    through this reduction at this efficiency, and the output
    speed
    past which it cannot make the required torque. On most designs here
    the first is smaller than the gearbox's own capacity — so the motor, not the
    contact stress, is what the drive is worth, and that comparison is now on one
    page.

  • The reduction can come from the motor. In the search dialog, work it out
    from the motor
    : state what the output has to do and the reduction stops
    being part of the question. design.ratio_band asks the curve which whole
    reductions can drive that load — closed form, one curve evaluation each, no
    geometry needed — and the search then works across a spread of them and ranks
    the results together. A reduction is a means; the job is a torque at a speed.

    The band's two ends fail for different reasons and it says which. Below it the
    motor is short of torque and gearing down further helps. Above it the motor is
    short of speed, and gearing down further is what caused it — so the answer
    there is bus voltage or a different motor, and a design that fails both ends
    at once is a motor that cannot do the job at any reduction. Told apart by
    asking whether the next reduction up improves the margin, rather than by
    whether the speed ceiling has been reached: past the peak the motor is often
    still making torque, just less than the speed has taken away, and calling that
    a torque shortage sends somebody to gear down when gearing down is what did
    it.

    What the search leaves out, it says: a feasible band is usually dozens wide
    and each one costs a full search, so five are sampled and the rest are
    reported in the rejection tally. A bounded search that looks exhaustive is how
    somebody concludes a reduction does not work when it was never tried.

    On the command line it is --ratio-from-motor --out-rpm 10, and the curve
    comes off the design passed with --design rather than off eight more flags —
    the app already has a place to put those eight numbers, and a second way to
    state them would be a second thing to keep in step.

  • motor_margin joins the batch metrics, so a study can sweep bus voltage
    or reduction against what the motor can hold. nan where no curve was stated
    — the same rule the fatigue margin follows on a printed part, because a study
    that sweeps a motor across a design that has none should come back empty
    rather than come back passing.

Changed

  • GearSpec.motor is now GearSpec.motor_face. It returns the mounting
    face, and an analysis result now carries the motor's own answer under motor;
    leaving both called the same thing would have put two different objects behind
    one word on the two most-read classes in the app. MotorFrame and
    MOTOR_FRAMES moved to the new core/motor.py beside the curve and are
    re-exported from core.spec, so every existing import still resolves.

  • The panel has a Motor group. The frame and motor turns the cam used to
    be filed under Mounting with the tie bolts, because a bolt pattern is what
    they did to the geometry. That put the two halves of "which motor" in
    different boxes the moment there was a second half. Fields the chosen kind
    does not use are greyed rather than hidden — the search box already owns
    visibility in that panel, and a field that vanishes takes the reason it does
    not apply with it.

Fixed

  • A headless run wrote its files and then died on the way out, on Windows.
    import cadquery pulls casadi in whether anything uses it or not — the
    PyInstaller build already stubs it out of the frozen bundle for exactly that
    reason — and with casadi 3.8.0 the two native libraries corrupt the heap while
    they unload. The process does its work, prints its results, and exits
    0xC0000374. Nothing here calls casadi, so nothing here could avoid it;
    casadi<3.8 is pinned on Windows until the unload order is fixed upstream.

    It matters more than a red tick on a dashboard: every scripted run reported
    failure having succeeded, which is the worst way for a tool to be wrong.

    Measured rather than suspected, and it took the measurement to get there. The
    suite ended 1048 passed, 1 skipped and the step then said "exit code 1" with
    nothing else in the log, which reads as a test failure that has hidden itself.
    Four experiments said what it was not: the same failure on v7.5.0, code that
    had been green a fortnight earlier; the same failure with PySide6 pinned back
    to the version that was green; the same failure on every one of sixteen test
    modules run in its own process, and none on the other twenty-three. What the
    sixteen had in common was the CAD kernel. Then two lines were enough:
    python -c "import cadquery" crashes, python -c "import casadi" does not,
    and import cadquery on casadi 3.7.2 is clean.

  • The test step now reports the exit code it was given. The default shell on
    Windows is pwsh, which GitHub runs with $ErrorActionPreference = 'stop', and
    since PowerShell 7.4 that turns any native command failure into a terminating
    error and a flat 1. The real code was -1073741819. A step that cannot tell
    you which of those it saw is a step that cannot be debugged, and this one was
    hiding the only fact that mattered.

  • The Qt test modules destroy their windows. test_workspace.py built
    sixty-six top-level windows and took down none of them, test_view3d nine,
    test_notice one — each holding a 3D view, matplotlib canvases and a worker
    thread, all left for the interpreter to take apart at exit in whatever order
    Python's collector reached them. Two other test modules here have always used
    deleteLater; these are the ones that never did. They are hidden and deleted
    after each test now — hidden rather than closed, because closeEvent writes
    the workspace to the preferences and a teardown that silently saved state
    would hand the next test a window restored from one it never asked about.

    It was not the crash above, and it was worth doing anyway: the suite went from
    32:49 to 15:35 on the Windows runner, and from 17:53 to 8:42 locally. Sixty-six
    live windows were not only a bad way to end a process, they were a tax on every
    test that ran after them.

  • The drawing's two caption lines used to run across the gearbox. They sat
    in the bottom-left corner of the axes, on the argument that the housing
    circle leaves that corner empty. It does, and neither line is short enough to
    stay in a corner: set_aspect("equal") makes the axes a square in the middle
    of a wide panel, the circle is inscribed in it, and a full line of monospace
    starting at the left edge runs straight under the circle and out the other
    side — across the disc on a 1560 px window and across the whole gearbox on a
    1180 px one. A corner is a place for a word, not for a sentence.

    Both lines are figure text now, in a strip tight_layout is told to keep off,
    and the strip is computed from the point size and the panel height rather than
    written down as a fraction — the same reason the layout is already re-solved
    on resize. Reserving the room rather than nudging the text is what makes it
    hold on all three canvases this one figure is drawn on: the app's letterbox
    panel, the PDF's square and the animation's small frame. Tested at four
    shapes, and what is asserted is that the drawing stops above the strip
    rather than where the text happens to be.

  • The 3D tab opened with the gearbox a third of the height of its own
    viewport.
    VTK's ResetCamera fits a sphere around the bounding box, and
    a cycloidal drive is a flat cylinder — so the diagonal it is sized by is most
    of a diameter longer than anything on screen, and the camera ended up that
    much too far back. The software renderer had never had this problem: it
    projects the vertices themselves onto the screen axes and asks how far back it
    has to be to hold those, which is a millisecond once per design. The GPU
    view uses the same call now, with the same field of view, so a design looks
    the same size whether or not the machine has a usable GPU — and the view opens
    on a gearbox instead of on a small object in an empty room.

  • The empty COMPARE tab is a panel that explains itself. The invitation to
    pin a reference was one sentence in the top-left corner with four hundred
    pixels of nothing under it, because one label was being asked to be both that
    invitation and the header over the filled table. They are two messages with
    two jobs; they are two labels now, and the empty one sits in the middle of the
    panel it is about to fill — which is what the trade study next door has always
    done, and it matters more here, because pinning a reference is the one feature
    of this window that nothing else in it mentions.

  • The remembered tab is stored by name now, not by position. A tab added in
    the middle renumbers every one after it, so a stored 4 reopened the session
    on somebody else's tab — which is exactly what putting MOTOR between
    EFFICIENCY and DATASHEET would have done to everybody who had the app open on
    the datasheet. The name survives being reordered and says what it means in a
    preferences file somebody may one day have to read. Preferences written by an
    earlier version still hold an integer and it is still read, so an upgrade does
    not lose the tab it was left on. The log tab's unread badge is stripped before
    the name is stored, because a session saved under LOG !! would never be
    found again by a window whose log is quiet.

  • The motor curve was being sampled at the wrong speed — caught by its own
    test before it shipped, and worth recording because the mistake is a
    reasonable one. GearSpec.crank_rate is crank angle per input revolution in
    the ring-fixed frame the kinematics are parameterised in
    , which is how every
    relative speed inside the drive is stated and is not a shaft speed at all. It
    is not 1 on a ring-output drive, so multiplying by it read the curve 4.5% off
    on a 21:1 and further out the lower the ratio. The motor turns the input shaft
    at the input speed, whichever member is grounded.


Windows - cycloidgen_..._Setup.exe. It is unsigned, so
SmartScreen will warn on first run: More info -> Run anyway. A
signed build is planned.

Linux - cycloidgen-...-x86_64.AppImage. chmod +x and run it;
it needs glibc 2.35 or newer. If it will not start, the machine has no
FUSE 2: install libfuse2, or run the file with
--appimage-extract-and-run, which unpacks it instead of mounting it.

macOS - cycloidgen-...-arm64.dmg, Apple silicon. Drag it to
Applications, then run this before opening it:

xattr -dr com.apple.quarantine /Applications/cycloidgen.app

It is signed ad-hoc but not notarized, so Gatekeeper blocks the first
launch. On macOS 15 and later the dialog it raises defaults to
Move to Trash, which deletes the install - there is no Open
Anyway
in that dialog, it appears in System Settings only after a
launch has been blocked, and right-click -> Open no longer bypasses
it. Clearing the flag first avoids the dialog entirely. On an Intel
Mac, use pip.

Anywhere with Python 3.10-3.14: pip install cycloidgen, then
cycloidgen.

The numbers this produces are preliminary sizing estimates, not a
certification. Validate against a physical prototype before anything
load-bearing depends on them.