Changelog

All notable changes to pylinkage are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. What the version number promises, and how a name is retired, is in the Deprecations page.

[Unreleased]

[1.2.2] - 2026-09-18

Fixed

  • Mechanism.set_constraints() now changes the mechanism. Link lengths live in per-link distance caches and in the Assur-group solver’s Dimensions, both captured when the mechanism is built; set_constraints only moved the crank tip, so the next step() snapped every joint back to the original geometry and get_constraints() read the old lengths. Every optimizer run on a Mechanism was a no-op since 0.8.0. The values are now written to the caches (new Link.set_distance()), the group solver is rebuilt, and Link.length reads the maintained constraint like DriverLink.radius does. Only binary links and driver radii are part of the constraint vector, as before.

[1.2.1] - 2026-09-12

Added

  • The documentation’s code runs in CI. tests/docs executes every script under docs/examples/ and every python block of every page under docs/source/tutorials/, in a subprocess with DeprecationWarning as an error. Marked docs and deselected from the default pytest run (minutes, every optional backend); the “Executable docs” workflow runs them next to the notebooks. Four examples and ten tutorials had been broken for a release without anything noticing.

Fixed

  • compute_mobility / compute_dof count rigid bodies, not graph elements. They used to count every edge and every hyperedge as a link and every node as a joint, which is only true of the topology catalog’s hyperedge-only graphs. Any geometric graph — a coupler point, a Hyperedge labelling a triangle of edges, what Linkage.to_hypergraph() emits for a FixedDyad, every classical walker leggedsnake ships — came out with nonsense (8 DOF for a coupler four-bar, 17 for a Jansen leg). Links are now the ground, one body per edge and per hyperedge, merged whenever two share two or more nodes; a node in k bodies is k - 1 joints, so coupler points are not joints and multiple joints count; a PRISMATIC node is a slider block with a prismatic joint to its guide hyperedge. The 19 catalog entries are unchanged; a grounded triangle now reports DOF 0 and a lone crank DOF 1, which the old tests documented as wrong.

  • HypergraphLinkage.to_simple_graph() expands a hyperedge to its clique, one edge per pair of nodes, as topology.isomorphism and assur.from_hypergraph already did. It produced a chain of N - 1 edges, so to_mechanism() could not solve a ternary link written as a hyperedge alone (“Could not determine solve order”) unless its edges were repeated by hand. Pairs already joined by an edge are kept, not duplicated. Hyperedge.to_edges() still returns the documented chain.

  • The graph tutorial’s mobility section describes the counting rules and no longer tells the reader to analyze a coupler mechanism “without the coupler point”.

[1.2.0] - 2026-09-12

Added

  • A defined public surface. A name is public when it is in a package’s __all__; modules inside a package are implementation; each name has one home. The rule is on the Deprecations page, and the surface is pinned name by name in tests/test_public_api.py, so it cannot change without a diff in that file. Newer subsystems are marked provisional there (topology, solver, co-design, multi-topology synthesis, hierarchical hypergraphs, CAD export): exported and documented, but not yet promised.

  • Every subpackage is reachable from the top level (pylinkage.components, pylinkage.hypergraph, pylinkage.optimization, …; before, only assur, dyads, mechanism, symbolic and synthesis were), and the definition path is importable straight from pylinkage: Ground, PointTracker, Crank, ArcCrank, LinearActuator, RRRDyad, RRPDyad, PPDyad, FixedDyad, alongside Linkage.

  • A deprecation policy, at docs/source/deprecations.md. One page stating what pylinkage promises about public names: a name is announced with a DeprecationWarning naming its replacement and removal release, and is removed no earlier than the next major version. It carries the table of everything currently deprecated, and how to surface the warnings, which are silent by default in Python.

  • pylinkage._deprecation, the machinery behind that: deprecated names are removed from their module’s namespace and served by a module-level __getattr__ (PEP 562), so reading one warns while from ... import Name keeps resolving exactly as before.

  • A “Which API should I use?” section in the README, naming components / actuators / dyads / simulation as the definition path and saying in one line each what MechanismBuilder, hypergraph, assur and Mechanism are for. The API reference index opens with the same guidance instead of an alphabetical list.

  • pylinkage.synthesis.crank_angle_limits(crank, coupler, rocker, ground), the angular range of a crank that cannot rotate fully. It sits beside grashof_check and is_crank_rocker, and returns None for a crank that rotates fully. It was synthesis.conversion._compute_crank_limits, a private name that only the editor’s backend ever called; it now has a public name and a test that checks the returned bounds against the collinear positions.

  • solution_to_linkage() honours FourBarSolution.arc_limits. The field existed but nothing in pylinkage read it. Set it — typically from crank_angle_limits() — and the linkage is built around an ArcCrank sweeping that range instead of a Crank, so a double-rocker or non-Grashof solution can be stepped and animated; a full turn would stop at an unbuildable position. The limits are relative to the ground line, as crank_angle_limits() returns them, and are placed on whichever side the crank starts.

  • orientation_resolution on path_generation(), replacing n_orientation_samples. It is the number of angles sampled per free orientation, so the cost model is now readable from the signature: the search grid holds orientation_resolution ** (n_precision_points - 1) candidates. The default of 6 reproduces the old default exactly.

  • A warning when path generation is about to be slow. Cost grows exponentially in the number of precision points, and nothing said so. Five points reach a four-dimensional grid that can run for seconds and still return nothing. Calls whose grid exceeds a thousand candidates now warn up front, and the message is repeated in SynthesisResult.warnings.

  • Ensemble.to_pareto_front() — the members as a ParetoFront, one objective per score column, for its best_compromise(), filter(), hypervolume() and plot(). The inverse of Ensemble.from_pareto_front().

  • PointTracker in the numba solver. It has FixedDyad geometry and now gets the same kernel; before, step_fast() silently froze a tracker at its initial position and returned that as its trajectory.

  • Slider rails in every drawing backend. pylinkage.visualizer.core gains get_rail_pairs() / build_rails(), and get_parent_pairs() knows RRPDyad.revolute_anchor, so a slider-crank draws its connecting rod and the line it slides on in matplotlib, SVG, DXF and STEP alike.

  • pylinkage.mechanism.TrackerJoint and ArcDriverLink are exported. Both leggedsnake and the editor’s backend were importing them from the mechanism.joint and mechanism.link modules because the package did not offer them.

  • pylinkage.dimensions and pylinkage.exceptions have an __all__ (Dimensions, DriverAngle; the four exception classes).

  • pylinkage.synthesis.BurmesterDyad, the new name for what was pylinkage.synthesis.Dyad. Same class, same behaviour; the old name still works and now warns.

Changed

  • 54 implementation names left the __all__ of their packages. They stay importable exactly as before; they are no longer advertised as public and import * no longer pulls them in. Among them: the numba kernels and step_single* helpers in solver (simulate, simulate_with_kinematics, SolverData, the JOINT_* constants and the conversion functions remain public); AnyJoint, AnyLink, the per-entity joint_to_dict / link_from_dict halves of mechanism_to_dict and is_legacy_format in mechanism; the Point2D / ComplexPoint / AnglePair aliases, point_to_complex, complex_to_point and compute_metrics in synthesis; SymCoord in symbolic; the styling constants and LinkStyle / SymbolType in visualizer; DYAD_TYPES, identify_dyad_type, identify_group_type in assur; DEFAULT_ANGULAR_VELOCITY in actuators; MutableAgent in optimization.collections; _AnchorProxy in components and dyads.

  • path_generation() is 2-8x faster, with byte-identical results. Verifying a candidate by simulating it was 67% of the runtime, and ran through the pure-Python Linkage.step(); it now uses the numba solver, which executes the same solve_* functions roughly twenty times faster. Measured end to end:

    Precision points

    Before

    After

    README example

    345 ms

    163 ms

    Benchmarked example

    1689 ms

    678 ms

    Three points

    430 ms

    55 ms

    The two paths report an unassemblable mechanism differently — step() raises, the solver writes NaN — so a naive switch would silently change which candidates survive. Both are treated as rejection, and the solution sets are verified identical across six point sets. step() is still used when numba is absent or the mechanism contains a component the solver cannot represent. This became possible only once step_fast() stopped returning NaN for FixedDyad, the component carrying the coupler point.

  • Linkage.step() is about a quarter faster — 257,550 to 325,718 steps/s on the reference four-bar — which speeds up every simulation, not only synthesis. Two things in the innermost loop: step() re-ran isinstance(component, (Crank, ArcCrank, LinearActuator)) for every component on every iteration although the classification is a property of the solve order, and _get_anchor_position ran an isinstance check against an abstract base class whose two branches returned the same expression. The published step_fast() speedup falls from 6.4x to 5.1x as a result: the numerator got faster, the solver did not regress.

Deprecated

  • The sibling re-exports in pylinkage.dyads: Ground, PointTracker, Component, ConnectedComponent, Crank, ArcCrank, LinearActuator and Linkage. Each still resolves to the same object and warns, naming its home (pylinkage.components, pylinkage.actuators, pylinkage.simulation). pylinkage.dyads.Ground and pylinkage.components.Ground were both correct, which is why tutorials disagreed on which to write.

  • pylinkage.optimization.Ensemble, in favour of pylinkage.population.Ensemble, its home.

  • pylinkage.assur.MobilityResult and StructuralAnalysis. Nothing in the package produced them; they were dataclasses exported without a function to fill them. The mobility analysis that exists is pylinkage.topology.compute_mobility(), returning a MobilityInfo.

  • n_orientation_samples, which never denoted a number of samples. It was folded into a per-axis grid resolution through max(6, round(n_samples ** (1 / free))), so the floor swallowed it: with four precision points every value from 6 to 216 produced an identical search. Measured on the README’s points, 6, 12, 36 and 72 all took the same time and returned the same ten solutions. It now emits a DeprecationWarning, is translated through the old formula so no result moves, and is scheduled for removal in 2.0.0. Use orientation_resolution.

    Redefining it to mean what its name says — roughly that many candidates in total — was implemented, measured and rejected: on three of six test point sets the search then returned nothing where it had found ten. Reordering the grid coarse-to-fine or shuffling it was also measured, in case truncation was merely sampling a biased corner; neither changed the solutions found nor the time taken. The dense grid earns its cost, so the search is unchanged and only the name was wrong.

  • The three Dyad names that were not what they said. Three unrelated classes were reachable as Dyad, and only two were dyads at all: pylinkage.assur.Dyad (an Assur group, unaffected and keeping its name), pylinkage.synthesis.Dyad (a Burmester dyad, now BurmesterDyad), and pylinkage.components.Dyad / pylinkage.dyads.Dyad with their ConnectedDyad counterparts, which were plain aliases of Component and ConnectedComponent — a Ground point is a Component, and calling it a dyad is simply wrong. All four aliases now emit a DeprecationWarning and are scheduled for removal in 2.0.0. Nothing breaks: each name still resolves to the same object it always did, and stays in its module’s __all__.

    The concrete cost of the collision was that Sphinx could not tell the three apart, so cross-references landed on the wrong class. With the aliases deprecated and the rename in place, the documentation build goes from 8 warnings to 0.

Removed

  • The api/ directory, the [api] extra and task api. The FastAPI server was the backend of pylinkage-editor and now lives there, in server/, with pylinkage as a dependency. It was never part of the wheel: pip install pylinkage[api] installed fastapi and uvicorn for code that was not there, and anyone setting up the editor had to find the server by reading this repository’s source (#46). If you were running it from a checkout, cd pylinkage-editor/server && uv run pylinkage-editor-server replaces uv run task api.

Fixed

  • Optimizers accept every linkage again. Ensemble compiled its numba solver template on construction, and since the solver started refusing dyads it cannot represent, every optimizer that returns an Ensemble (particle_swarm_optimization, multi_objective_optimization, …) raised NotImplementedError for a linkage holding a PPDyad, a cam follower, or leggedsnake’s Walker — none of which the optimizer itself needed the solver for. The template is now compiled on first use (Ensemble.template, simulate(), topology_key), so those linkages optimize as they did in 1.1.1 and the clear error only appears where batch simulation is asked for.

  • step_fast() silently returned NaN for every dyad except RRRDyad. linkage_to_solver_data() typed every dyad as JOINT_REVOLUTE and read distance1 / distance2 off it. Only RRRDyad has those attributes, so a FixedDyad — which has distance and angle — reached the solver as two zero-radius circles that never intersect, and the joint plus everything downstream of it came out NaN. No exception was raised: callers got a full-shaped array of NaN where positions should be, while step() computed the same mechanism correctly. Since a FixedDyad is how a traced point is rigidly attached to a coupler link, this covered most mechanisms worth simulating quickly, including everything pylinkage.synthesis emits. The conversion now dispatches on the actual dyad class: JOINT_FIXED with (distance, angle) for FixedDyad, JOINT_PRISMATIC with three parents for RRPDyad, JOINT_REVOLUTE only for RRRDyad. Both were already implemented in solver/simulation.py and simply unreachable. PPDyad and the cam followers, which the solver cannot represent, now raise NotImplementedError naming step() as the alternative rather than producing a wrong answer. update_solver_constraints() shares the same table, so the two cannot drift. step() and step_fast() now agree bit for bit on all three supported dyad types, and are tested to.

  • step_fast() refuses a component it cannot represent — a custom Component, say — with NotImplementedError naming it, instead of typing it as a fixed point and reporting a frozen trajectory.

  • The SVG, DXF and STEP exporters draw the component API. All three walked the legacy linkage.joints / joint0 / joint1 attributes, so plot_linkage_svg() produced joints and trajectories but no bars for a simulation.Linkage, and plot_linkage_dxf() / build_linkage_3d() raised AttributeError. They now share build_connections() with the matplotlib backend.

  • show_kinematics(), animate_kinematics() and plot_linkage_plotly_with_velocity() work on a simulation.Linkage. Same legacy attributes, plus an omega check that never saw the value set_input_velocity() stores; their tests wrapped the linkage in an adapter that mimicked the old API and passed around the bug.

  • Ensemble.show(), plot_plotly() and save_svg() no longer require a prior simulate(). They simulated the member on the spot and then looked for the trajectory in the batch cache, which the one-off simulation does not fill, and raised ValueError.

  • parallel_coordinates_plot() with unbuildable particles. A score of ±inf (the kinematic_* decorators’ penalty) made the score axis NaN; such particles now sit at the bad end of the axis.

  • pip install pylinkage works on Python 3.10. mechanism.builder imports Self from typing_extensions below 3.11 but the package never declared it, so a fresh 3.10 environment failed at import pylinkage.

  • The tutorials run. Ten of the eleven pages under docs/source/tutorials/ failed on 1.1.1: pylinkage.joints, keyword arguments that never existed (show_linkage(animated=…), generate_bounds(max_ratio=…)), len(result) on a SynthesisResult, tuple-unpacking an Ensemble, an invented hypergraph API, change-point four-bars that no perturbation survives. custom_joints, graph_representation and visualization are rewritten against the current API; the others are corrected in place, with expected outputs regenerated. Every code block of every tutorial now executes in order with DeprecationWarning as an error.

  • The example scripts run again. The three synthesis demos still called len(result) and if result: on a SynthesisResult, which lost those since 1.0; every demo animated whatever came out of synthesis with a full-turn Crank, which stops on a double-rocker; and the Grashof-type table in fourbar_from_lengths_demo.py labelled two change-point linkages as double-crank and double-rocker. The demos now animate through crank_angle_limits() + arc_limits, the table’s lengths match their labels, and three demos that always came back empty — five-position approximate function generation, the straight-line path (three collinear points are degenerate for Burmester theory), and the two fixed-ground-pivot demos (arbitrary frame points never lie on the center-point curve) — now show the workflow that does produce a linkage. All twelve scripts under docs/examples/ run to completion with DeprecationWarning as an error.

  • The benchmarks page figures are regenerated against the above, and its breakdown of path_generation() is rewritten: Burmester synthesis over the orientation grid is now 87% of the runtime and verification under 10%, the reverse of what the page described. It also now warns that cost grows exponentially in the number of precision points — five points can take seconds and return nothing — which no documentation said.

  • The benchmarks page gave wrong advice about path_generation(). It attributed the cost to the orientation sweep and said lowering n_orientation_samples trades coverage for time roughly linearly. Profiling shows neither is true: verifying candidates by simulation is 67% of the runtime and Burmester synthesis only 21%, and n_orientation_samples is close to inert — the values 6, 12, 36 and 72 take the same time and return the same ten solutions on the README’s points, because the per-axis grid resolution it feeds is floored at 6. The real control is max_solutions, measured from 124 ms at 1 solution to 2316 ms for all 79. The page and the path_generation() docstring now say so, and the page notes that cost depends heavily on which points are asked for: two four-point problems returning ten solutions each measured 336 ms and 1676 ms. Tracked in #29.

  • The cited DOI pointed at 1.0.0 and would have gone stale every release. CITATION.cff and the README badge carried 10.5281/zenodo.21207487, the version DOI Zenodo minted for 1.0.0. Zenodo also mints a concept DOI per project — 10.5281/zenodo.21207486 here — which never changes and always resolves to the newest version. Both now cite that instead, so neither needs touching at release time; the 1.0.0 version DOI is kept as a second identifier, since anything already citing it should keep resolving.

  • CITATION.cff was bumped by hand. Its version and date-released were not in the bump-my-version file list, so every release depended on remembering to edit them. They are now bumped automatically, the date from {now}. CONTRIBUTING.md’s release section is corrected to match, and gains the uv lock step it was missing — uv.lock records the project version and bump-my-version does not touch it.

[1.1.1] - 2026-09-04

Added

  • Public benchmarks. docs/source/benchmarks.md reports figures for the numba solver, PSO throughput, and the three synthesis entry points, together with the hardware they came from and the command that reproduces them. The harness behind it is benchmarks/run_benchmarks.py, which measures the public API only, takes the median of repeated runs, and discards warmup so numba’s one-off JIT compilation is not charged to steady-state figures. The pre-existing scripts in benchmarks/ are now marked as development explorations, since two of them benchmark reimplementations and a hypothetical joint rather than shipped code.

  • pylinkage.dyads.to_mechanism() is now public. It converts a component Linkage into a Mechanism, and previously existed only as pylinkage.dyads._conversion.to_mechanism, so it could not be used or documented without reaching into a private module. The conversion remains one-way: there is no Mechanism to Linkage direction, the old mechanism_to_linkage() having been removed with the legacy joints module.

Changed

  • One docstring style across the codebase. Docstrings were split between two conventions: 85 files used Google style (Args:) while 16 used reST field lists (:param:), and linkage/transmission.py used both at once. The 16 reST files — concentrated in symbolic/, optimization/ and geometry/, the oldest code in the package — are converted to Google style, so all 100 documented modules now read the same way. This is presentation only: 393 fields were rewritten with no change to wording, and the sole vocabulary removed across all 16 files is the param/returns/raises markers themselves. Sphinx cross-reference roles such as :func:`max` are untouched, being valid in Google-style docstrings.

  • The full extra now includes pymoo, so pip install pylinkage[full] covers multi-objective optimization as the README’s “all optional backends” description promises. moo remains available on its own, and is now listed in the README’s extras table.

Fixed

  • Documentation links that pointed at the wrong class. pylinkage.dyads exports Dyad as a plain alias of Component (and ConnectedDyad of ConnectedComponent), and the package annotated its anchors with that alias — revolute_anchor: Dyad | _AnchorProxy. Two unrelated classes are also named Dyad (pylinkage.assur.Dyad, an Assur group, and pylinkage.synthesis.Dyad, a Burmester construct), so Sphinx resolved those annotations to one of them and sent readers to a class the code never referred to. The annotations now name Component directly. The Dyad and ConnectedDyad aliases remain exported and unchanged, so no import breaks. Docs build warnings drop from 38 to 8.

  • Broken README links, on three surfaces at once. The README linked to the 15 tutorial notebooks, CONTRIBUTING.md and CODE_OF_CONDUCT.md with repository-relative paths. Those resolve only on GitHub: on the PyPI landing page and in the rendered documentation they were dead. They are now absolute GitHub URLs, matching how the README already linked its images. The in-page [tutorials](#tutorials) link is fixed by enabling myst_heading_anchors, so heading anchors resolve in the docs as they do on GitHub.

  • The README was rendered twice in the documentation. index.rst listed it in the toctree and pulled it in again with .. include::, so the front page duplicated the entire README and every warning it produced was counted twice. The inline include is removed; the README remains as its own page, linked first under Introduction. Docs build warnings drop from 76 to 38.

  • Docstrings rendered wrongly in the API reference. The codebase writes Google-style docstrings (Args:, Returns:, Raises:), but the docs never enabled sphinx.ext.napoleon, so every one of them was parsed as a definition list: parameter descriptions were swallowed as stray indentation, and *args / **kwargs were read as emphasis markup. Enabling napoleon renders them as proper parameter tables. napoleon_use_ivar is set alongside it so a documented attribute does not collide with the entry autodoc already generates for the same dataclass field, and package-level automodule directives are marked :no-index: so each object is indexed once rather than once per re-export. Nine genuinely malformed docstrings were fixed by hand: bullet lists and formula blocks missing their preceding blank line, isomer signatures such as RRT_ that RST read as link targets, and an empty Conversion: section in pylinkage.mechanism left behind when the mechanism/linkage conversion helpers were removed. Docs build warnings drop from 496 to 76, with none remaining from docstrings.

  • Stale API reference. docs/source/api/ was checked-in sphinx-apidoc output that had not been regenerated since before 1.0. It documented three modules that no longer exist — the legacy pylinkage.joints package removed with the joints API, plus pylinkage.collections and pylinkage.linkage.linkage — which failed to import on every docs build. More importantly it covered only 6 of the package’s 18 subpackages, so synthesis, mechanism, hypergraph, cam, symbolic, solver, components, actuators, dyads, simulation, assur, bridge, population and topology had no API reference at all. Regenerated against the current package.

  • Type information was not exposed to users. The package is fully annotated and passes mypy --strict on all 133 source files, but shipped no PEP 561 py.typed marker, so type checkers and editors in downstream projects silently ignored every annotation and treated pylinkage as untyped. The marker is now included in the wheel; no packaging change was needed, since [tool.hatch.build.targets.wheel] already covers the whole package directory.

  • The front-page example did not say what to install. The first snippet in the README — the one PyPI shows above the fold, and the one reported in #25 — calls path_generation() and show_linkage(), which live behind the scipy and viz extras. Every other example needing an extra said so; this one did not, so after a plain pip install pylinkage it raised ModuleNotFoundError before reaching any pylinkage code. It now names the install line it needs.

  • Broken examples on the PyPI landing page. Both README visualization snippets called plot_kinematic_linkage(linkage), but that function takes (linkage, fig, axis, loci) and always has — the signature is identical in 1.0.0, so these examples never ran. They now call show_linkage(), which builds the figure and runs the simulation itself. The PSO snippet separately used get_num_constraints() / set_num_constraints(), removed earlier in the 1.x line; it now uses get_constraints() / set_constraints().

  • Tutorial notebooks failing to execute. Seven of the fifteen notebooks raised on execution, against removed API: get_num_constraints() / set_num_constraints() (notebooks 02, 03, 09, 10, 11, 13), SymbolicLinkage.components in place of .joints (05), and mechanism_to_linkage() (10). Notebook 10 additionally described the component/dyad Linkage as a “legacy” API throughout; the legacy API was pylinkage.joints, which no longer exists. A Notebooks CI workflow now executes every notebook on push and pull request.

[1.1.0] - 2026-08-12

Fixed

  • Velocities and accelerations no longer treat an unknown anchor as a stationary one. Mechanism.step_with_derivatives read each anchor’s kinematics as anchor.velocity or (0.0, 0.0). A joint solved against an anchor whose own velocity was undefined was therefore solved against one wrongly assumed at rest, and returned a plausible number instead of None — so a single unresolved joint quietly corrupted everything downstream of it rather than announcing itself. Checked against finite differences of the position stream, one leggedsnake walker had a foot reporting ~5x its true speed and another ~0.3x. Unknown kinematics now propagate: if an anchor’s velocity or acceleration is undefined, so is the dependent joint’s.

    Ground joints are unaffected — their zero velocity is a fact about the frame, not a stand-in for a missing value.

  • A joint collinear with its two anchors is no longer mistaken for a dead centre. solve_revolute_velocity intersects the two differentiated distance constraints, whose Jacobian is singular when the joint lies on the line through its anchors. That test conflated two unrelated situations: a genuine toggle of an RRR dyad, where the velocity really is indeterminate, and a joint riding on a rigid body together with both its anchors, where the motion is fully determined and only the formulation degenerates. The second is a normal design — a ternary link carrying its coupler point on the line through its other two ports, as in the Chebyshev and Hoeken straight-line linkages — and it silently produced no derivative at all.

    Such joints are now resolved by propagating the body’s motion. The two cases are told apart structurally rather than numerically: the fallback applies only when a link holds the two anchors at a fixed distance, so a real dead centre still yields None.

Added

  • solve_rigid_body_velocity / solve_rigid_body_acceleration in pylinkage.solver. Given two points of a planar rigid body and their kinematics, these return the velocity and acceleration of any third point on that body. They agree with the constraint-intersection solvers wherever both apply, and stay well-conditioned for collinear points, where those are singular — they need two distinct points on the body rather than two independent constraint directions.

[1.0.0] - 2026-07-05

Added

  • multi_objective_optimization parallel evaluation. New n_workers and linkage_factory keyword arguments route candidate evaluation through a concurrent.futures.ProcessPoolExecutor when n_workers > 1. linkage_factory is an escape hatch for linkages that are not picklable (e.g. carry cached numba SolverData): each worker builds its own linkage via the factory instead of receiving a pickled copy. This unblocks downstream packages (notably leggedsnake) dropping their custom parallel NSGA problem wrappers.

  • simulation.Linkage.to_hypergraph() (+ the module-level :func:pylinkage.hypergraph.from_sim_linkage). Converts a modern simulation.Linkage (the object produced by synthesis and co_optimize) into a (HypergraphLinkage, Dimensions) pair so downstream hypergraph-native consumers (notably leggedsnake.Walker.from_synthesis) can ingest synthesis output directly without a local shim. Covers Ground, Crank, ArcCrank, LinearActuator, RRRDyad, FixedDyad, RRPDyad and PPDyad; unknown component types raise NotImplementedError instead of silently dropping topology.

  • Dimensions.to_dict / Dimensions.from_dict (and matching DriverAngle.to_dict / DriverAngle.from_dict). Returns a JSON-safe representation so downstream consumers (notably leggedsnake.serialization) can drop their manual hyperedge-key stringification helpers. Hyperedge pairwise constraints are emitted as [node_a, node_b, distance] triples; from_dict also accepts the legacy "('a', 'b')" stringified-tuple form for back-compat with previously saved files.

  • Modern-container parity with the legacy Linkage. Both pylinkage.simulation.Linkage and pylinkage.mechanism.Mechanism now carry the full kinematic and analysis surface the legacy class used to expose:

    • compile() + step_fast() — pre-compile the numba SolverData once and reuse it across calls (via pylinkage.bridge).

    • step_fast_with_kinematics() — batched numba simulation returning (positions, velocities, accelerations) trajectory arrays.

    • set_input_velocity(driver_or_crank, omega, alpha=0.0) + get_velocities() / get_accelerations() — per-joint velocity/acceleration accessors.

    • step_with_derivatives() — per-step Python path that computes velocities and accelerations via solver.velocity / solver.acceleration.

    • analyze_transmission() / analyze_stroke() / analyze_sensitivity() / analyze_tolerance() — bound-method shims over the free functions in pylinkage.linkage.

    • transmission_angle() / stroke_position() — single-shot accessors at the current pose.

    • set_completely(constraints, positions) — apply a flat constraint vector and joint positions in one call.

    • simulation(iterations=None, dt=1.0) — context manager that restores the initial joint positions on exit (new helper in pylinkage._simulation_context.Simulation).

    • indeterminacy() — planar Gruebler-Kutzbach mobility (a standard Grashof four-bar returns 1).

  • Mechanism cross-API aliases. get_coords / set_coords now work on a Mechanism as well, delegating to the native get_joint_positions / set_joint_positions.

  • Mechanism.rebuild(initial_positions=None) — matches simulation.Linkage.rebuild. Optionally writes new joint positions and always clears the cached SolverData so the next step_fast() recompiles.

  • Mechanism.step(iterations=None, dt=1.0) — accepts an iterations keyword (matching both legacy and modern Linkage).

  • Joint.velocity / Joint.acceleration (runtime state, compare=False) on all Mechanism joints — populated by step_with_derivatives and step_fast_with_kinematics.

Changed

  • LinkageProblem reuses one process pool across generations. LinkageProblem now creates the ProcessPoolExecutor lazily on the first parallel batch and reuses it for every subsequent batch. multi_objective_optimization calls problem.close() in a finally block so workers don’t outlive the optimization. The previous code forked N workers per generation, taxing every batch with ~50–500 ms of pool startup (heavier still when worker imports are large). Apples-to-apples savings scale with generation count; expect 15–25 % wall-time wins at n_workers 4 on multi-gen runs. Behaviour is unchanged for n_workers == 1.

  • pylinkage._compat now targets only the modern surface. The joint-legacy branches (Static / _StaticBase / Revolute / Pivot / Fixed / Prismatic / Linear name matches) were dead after phase 2c; is_ground / is_dyad now only recognise the modern component classes and the Mechanism joint types.

  • pylinkage._compat: is_ground now recognises a Mechanism GroundJoint; is_driver recognises a RevoluteJoint that sits as the output of a DriverLink/ArcDriverLink; is_dyad recognises a non-ground, non-driver RevoluteJoint/PrismaticJoint. The container-agnostic analysis helpers in pylinkage.linkage.* now auto-detect four-bar joints on a Mechanism without additional hints.

  • pylinkage.bridge.solver_conversion.linkage_to_solver_data gains a _mechanism_to_solver_data dispatch path. Mechanism’s Links-on-constraints data model is now translated to SolverData for numba simulation (driver outputs become JOINT_CRANK with radius/angular-velocity pulled from the owning DriverLink; driven revolute joints become JOINT_REVOLUTE with anchor distances walked from the joint’s _links).

  • pylinkage.bridge.solver_conversion: collapsed to a single compatibility-agnostic implementation that dispatches through pylinkage._compat — no more legacy joint-type dispatch.

  • pylinkage.synthesis.nbar_solution_to_linkage / _generic_nbar_to_linkage: now build a modern pylinkage.simulation.Linkage from the component/actuator/dyad API instead of a legacy joint-based Linkage.

  • pylinkage.synthesis.linkage_to_synthesis_params: accepts the component API only; raises ValueError for legacy linkages.

Removed

  • pylinkage.hypergraph._types re-export module (deprecated since 0.8.0). Import the canonical types from pylinkage._types (JointType, NodeRole, NodeId, …) directly. All internal callers have been migrated.

  • pylinkage.solver.JOINT_LINEAR constant. This was always an alias for JOINT_PRISMATIC = 4; the duplicate name has been removed. Use JOINT_PRISMATIC.

  • "Linear" entry in pylinkage.visualizer.SYMBOL_SPECS and the matching "Linear" branch in the auto-detect path inside pylinkage.linkage.transmission. These matched the legacy joints.Linear class name, which is gone. Modern prismatic components are resolved through "Prismatic" / "RRPDyad" / "LinearActuator".

  • get_num_constraints / set_num_constraints on both simulation.Linkage and Mechanism. These deprecated wrappers were added to ease the rename to get_constraints / set_constraints and are now gone. Calling them raises AttributeError.

  • pylinkage.linkage.Linkage and pylinkage.linkage.Simulation: the legacy Linkage class is gone. pl.Linkage now points at :class:pylinkage.simulation.Linkage (component/actuator/dyad API); pl.Simulation points at the shared :class:pylinkage._simulation_context.Simulation context manager. Internal TYPE_CHECKING imports that referenced pylinkage.linkage.Linkage have been repointed to pylinkage.simulation.Linkage. User code that built linkages via pl.Linkage(joints=[...]) must migrate to the component API — see the migration notes in the 1.0.0 notebooks and tutorials.

  • pylinkage.joints module (legacy joint API — Static, Crank, Revolute, Pivot, Fixed, Prismatic, Joint). Deprecated since 0.7.0 (Pivot since 0.6.0). Use the component/actuator/dyad API: pylinkage.components.Ground, pylinkage.actuators.Crank, pylinkage.dyads.RRRDyad / FixedDyad / RRPDyad. Top-level re-exports (pl.Static, pl.Crank, …) are gone.

  • pylinkage.linkage.Linkage.to_dict/from_dict/to_json/from_json: serialization of legacy joint-based linkages is no longer supported. Use pylinkage.mechanism.mechanism_to_dict/from_dict on a Mechanism instead.

  • pylinkage.linkage.serialization: module removed — served legacy joints only.

  • pylinkage.mechanism.mechanism_from_linkage / mechanism_to_linkage / convert_legacy_dict: bridged the legacy LinkageMechanism models, neither of which needs the bridge now that the legacy joint API is gone. Use pylinkage.mechanism.fourbar and friends to build a Mechanism directly.

  • pylinkage.hypergraph.from_linkage and pylinkage.assur.linkage_to_graph: same rationale as the legacy to_linkage() / graph_to_linkage() removed in 0.9.0. Use from_mechanism / mechanism_to_graph respectively.

  • pylinkage.symbolic.linkage_to_symbolic / symbolic_to_linkage: removed. Build SymbolicLinkage directly with :class:SymCrank / :class:SymRevolute / :class:SymStatic, or use :func:fourbar_symbolic.

  • pylinkage.optimization.grid_search.tqdm_verbosity(): overdue since 0.7.0. Use tqdm.tqdm(iterable, disable=not verbose) directly.

  • SynthesisResult.__len__ / __iter__ / __getitem__ / __bool__: deprecated in 0.9.0. Access result.solutions (or result.ensemble for batch operations) instead — e.g. len(result.solutions), for linkage in result.solutions, result.solutions[i].

  • pylinkage.hypergraph.to_linkage(): deprecated in 0.8.0. Use pylinkage.hypergraph.to_mechanism() for conversion to the current Mechanism model.

  • pylinkage.assur.graph_to_linkage(): deprecated in 0.8.0. Use pylinkage.assur.graph_to_mechanism() for conversion to the current Mechanism model.

Fixed

  • multi_objective_optimization single-objective runs. pymoo returns the single best solution with F shape (n_obj,) and X shape (n_var,) when n_obj == 1, not (n_pop, n_obj). The previous code indexed result.F[:, k] unconditionally and crashed. Results are now normalised to 2-D before building the Ensemble so n_obj == 1 works uniformly.

  • Solver-cache invalidation on constraint mutation. simulation.Linkage.set_num_constraints and Mechanism.set_constraints now clear _solver_data before applying the new constraints, so a subsequent step_fast() rebuilds the numba arrays. Without this, optimizers that round-tripped candidate constraints would silently keep simulating the previous parameters.

[0.9.0] - 2026-04-14

Added

  • extract_trajectory(loci, joint=-1) in pylinkage.linkage.analysis (re-exported from pylinkage): returns (xs, ys) numpy arrays for one joint’s path, skipping unbuildable frames. Replaces the recurring [(p[i][0], p[i][1]) for p in loci if p[i][0] is not None] boilerplate. Accepts integer index, joint name, or joint instance (when linkage is given).

  • extract_trajectories(loci, linkage=None): all-joints variant of the above. Returns {joint_name: (xs, ys)} when linkage is given, or {index: (xs, ys)} otherwise. Skips None frames per joint.

  • pylinkage.mechanism.fourbar(crank, coupler, rocker, ground, ...) and slider_crank(crank, rod, ...) factory functions: collapse the eight-line MechanismBuilder chain for the canonical four-bar and slider-crank topologies into a single call, returning an assembled Mechanism.

  • TransmissionAngleAnalysis.plot(ax=None): one-line replacement for the matplotlib boilerplate (axhline at the acceptable-range bounds, the 90° optimum, fixed [0, 180] y-axis, crank-angle x-axis). Accepts an existing axes for use inside subplot grids.

  • Population abstractions for batch mechanism work (pylinkage.population):

    • Member: universal single-mechanism record (dimensions, scores, trajectory). to_loci() converts trajectories to the tuple format the visualizer expects.

    • Ensemble: topology-bound population — one linkage structure with N parameter variants. Batch simulation via the numba solver, numpy-style indexing (ens[i] → Member, ens[1:3] → Ensemble), columnar scores for vectorized rank(), top(), filter(), filter_by_score(). Visualization shortcuts: show(), plot_plotly(), save_svg().

    • Population: heterogeneous collection of Ensembles, keyed by topology label. simulate_all(), rank(), top() across topologies. from_members() auto-groups by topology key. from_topology_solutions() wraps multi-topology synthesis results with QualityMetrics as score columns.

    • SynthesisResult.ensemble property: lazily builds an Ensemble from synthesis solutions with link lengths as score columns.

  • skip_unbuildable mode for Linkage.step(): new boolean parameter that catches UnbuildableError per iteration and yields None-coordinate tuples instead of aborting the entire simulation. Non-Grashof and double-rocker linkages now recover the valid trajectory on both sides of dead zones.

  • Dual Annealing optimizer: dual_annealing_optimization() wraps scipy’s generalized simulated annealing — a single-trajectory global optimizer effective for problems with many local minima and expensive evaluations.

  • Optimizer chaining: chain_optimizers() runs multiple optimizers in sequence, automatically feeding each result as the starting point for the next stage. Common pattern: global search (DE/PSO) → local refinement (Nelder-Mead).

  • Co-optimization of topology + dimensions:

    • Mixed-variable evolutionary optimizer (co_optimize()) jointly searching discrete topology space and continuous dimensional space using NSGA-II/III via pymoo with custom genetic operators.

    • Topology neighborhood graph (build_neighborhood_graph(), topology_neighbors(), topology_distance()) defining adjacency between all 19 catalog topologies via add_dyad, remove_dyad, swap_variant, and restructure operations.

    • Custom pymoo operators: MixedCrossover (BLX-alpha blend + topology swap), MixedMutation (Gaussian perturbation + topology neighbor mutation), warm_start_sampling() (seed population from Phase 3 synthesis results).

    • Virtual edge encoding: expands hyperedges (ternary links) into pairwise distances and adds implicit ground-link virtual edges for chromosome representation.

    • Simultaneous triad placement via scipy.optimize.least_squares for topologies with circular dependencies (e.g., Stephenson six-bar).

    • Warm-start pipeline: warm_start_co_optimization() converts Phase 3 TopologySolution results to MixedChromosome seeds for NSGA-II.

    • New types: MixedChromosome, CoOptimizationConfig, CoOptSolution, CoOptimizationResult.

    • TopologyCatalog.topology_index() and topology_by_index() for integer-indexed topology lookup.

  • Triad solving in mechanism simulation: Mechanism.step() now uses Assur group decomposition internally, solving dyads and triads via solve_group() dispatch. Six-bar linkages (Watt and Stephenson types) can be simulated end-to-end. graph_to_mechanism() handles triad groups (2 internal nodes, 4+ edges), creating the appropriate joints and links.

  • Convenience builders for six-bar linkages:

    • watt_from_lengths(): build a Watt six-bar from seven link lengths + ground length. Returns a SimLinkage ready for simulation.

    • stephenson_from_lengths(): build a Stephenson six-bar from the same parameter pattern. Both include ASCII kinematic chain diagrams in docstrings.

    • Exported from pylinkage.synthesis alongside fourbar_from_lengths().

  • Topology enumeration:

    • Graph isomorphism detection via WL-1 color refinement + backtracking verification: canonical_form(), canonical_hash(), are_isomorphic().

    • Systematic enumeration of all non-isomorphic 1-DOF planar linkage topologies up to 8 links: enumerate_topologies(), enumerate_all(). Validated against Mruthyunjaya 1984: 1 four-bar + 2 six-bars + 16 eight-bars = 19.

    • Built-in topology catalog (TopologyCatalog, CatalogEntry, load_catalog()) with JSON-serialized HypergraphLinkage graphs and metadata (link assortment, family, joint count).

    • All new symbols exported from pylinkage.topology.

Fixed

  • compute_dof hyperedge counting: compute_mobility() counted each hyperedge with k nodes as (k−1) links instead of 1 rigid body, giving wrong DOF for any mechanism with ternary or higher links (all six-bars and eight-bars).

Deprecated

  • SynthesisResult collection protocol: len(result), result[i], for linkage in result, and bool(result) now emit DeprecationWarning. Use result.ensemble instead. Will be removed in 1.0.0.

Changed

  • fourbar_from_lengths() now returns SimLinkage (from pylinkage.simulation) instead of the legacy Linkage (from pylinkage.linkage). The new object uses the component/actuator/dyad API: access joints via .components instead of .joints. The .step() method is unchanged. linkage_to_synthesis_params() accepts both old and new linkage types.

  • All optimization functions now return Ensemble instead of list[Agent], list[MutableAgent], or ParetoFront. Affected functions: particle_swarm_optimization(), trials_and_errors_optimization(), differential_evolution_optimization(), dual_annealing_optimization(), minimize_linkage(), chain_optimizers(), multi_objective_optimization(), and all async variants. Migration: replace score, dims, pos = result[0] (Agent tuple unpacking) with member = result[0]; member.score.

  • Default simulation resolution increased from ~63 to 360 steps per rotation: The default angular velocity for Crank, ArcCrank, DriverLink, and ArcDriverLink changed from 0.1 rad/step to tau / 360 (~0.01745 rad/step), giving one sample per degree. The Mechanism.get_rotation_period() fallback and all synthesis/visualizer iteration defaults changed from 100 to 360 accordingly. A DEFAULT_ANGULAR_VELOCITY constant is now exported from pylinkage.actuators.

  • PSO is now pure NumPy: particle_swarm_optimization() no longer depends on pyswarms (unmaintained since 2021). Replaced with a built-in local-best ring-topology PSO. The pso optional extra is kept but empty for backwards compatibility. The API is unchanged.

  • Renamed abbreviated parameters for clarity (#17): All old names are still accepted as keyword arguments for backwards compatibility.

    • itersiterations in particle_swarm_optimization() and its async variant.

    • posinitial_positions in Linkage.rebuild().

    • init_positionsinitial_positions in Agent, MutableAgent, and ParetoSolution.

Removed

  • HypostaticError alias removed. Use UnderconstrainedError directly (alias was deprecated since 0.7.0).

  • Linear joint alias removed. Use Prismatic directly (alias was deprecated since 0.7.0).

  • pyswarms dependency removed from all extras (pso, full, dev group).

[0.8.0] - 2026-03-28

Added

  • Multi-objective optimization:

    • New multi_objective_optimization() function using NSGA-II/NSGA-III algorithms via pymoo.

    • ParetoFront class for storing and analyzing non-dominated solutions.

    • ParetoSolution dataclass for individual Pareto-optimal solutions.

    • Pareto front visualization with pareto.plot().

    • Hypervolume indicator computation with pareto.hypervolume().

    • Best compromise solution selection with pareto.best_compromise().

    • Crowding distance-based filtering with pareto.filter().

    • New optional dependency group: pip install pylinkage[moo].

  • Cam-follower mechanisms:

    • New pylinkage.cam module with motion laws and profile definitions.

    • Motion laws: HarmonicMotionLaw, CycloidalMotionLaw, ModifiedTrapezoidalMotionLaw, PolynomialMotionLaw (with polynomial_345() and polynomial_4567() factory functions).

    • Profile types: FunctionProfile (motion law-based) and PointArrayProfile (spline interpolation).

    • TranslatingCamFollower dyad for linear follower motion driven by cam rotation.

    • OscillatingCamFollower dyad for rocker arm motion driven by cam rotation.

    • Both knife-edge (roller_radius=0) and roller followers supported.

    • Numba-compiled profile evaluation for high-performance simulation.

  • Triad (Class II) Assur groups:

    • New Dyad and Triad classes parameterized by signature string, replacing the per-type classes (DyadRRR, DyadRRP, etc.) which are kept as aliases.

    • signature_to_hypergraph() now generates triad topologies (6-joint signatures).

    • decompose_assur_groups() detects triads when no dyad can be formed, enabling decomposition of six-bar mechanisms (Watt and Stephenson types).

    • Solver dispatches by solver_category (circle-circle, circle-line, line-line) instead of isinstance(), automatically supporting new group signatures.

  • Topology analysis:

    • New pylinkage.topology module with compute_dof() implementing Grübler’s formula (DOF = 3(n−1) 2j₁ j₂) on HypergraphLinkage.

    • compute_mobility() returns full MobilityInfo (DOF, link count, joint counts).

  • SymPy for analytical optimization.

  • Native computation of velocity and acceleration with visualizations.

  • Linkage synthesis with Burgmester’s theory, function, path and motion generation.

  • Adds scipy.

    • Exact optimization solving (better than numpy) + support constraints.

    • Adds a new optimization: differential evolution.

  • High-level velocity/acceleration API:

    • Component.velocity and Component.acceleration properties on all components.

    • simulation.Linkage.set_input_velocity(actuator, omega, alpha) to set crank angular velocity.

    • simulation.Linkage.step_with_derivatives() generator yielding (positions, velocities, accelerations).

    • simulation.Linkage.get_velocities() and get_accelerations() batch query methods.

    • solver.step_single_acceleration() numba-compiled acceleration solver.

    • Exported acceleration solvers: solve_crank_acceleration, solve_revolute_acceleration, solve_fixed_acceleration, solve_prismatic_acceleration.

Fixed

  • PSO score sign: particle_swarm_optimization() returned the negated pyswarms cost when order_relation=max, producing incorrect (often negative) scores.

  • Mechanism builder branch selection: MechanismBuilder.set_branch() produced inconsistent assembly configurations because circle-circle constraints arrived in non-deterministic order depending on which connected port was solved first. Constraints are now sorted by center position before intersection, making branch 0/1 deterministic.

Changed

  • Breaking: Dropped Python 3.9 support. Minimum version is now Python 3.10.

  • Added Python 3.14 to CI test matrix.

  • Breaking: Linkage.step_fast_with_kinematics() now returns a 3-tuple (positions, velocities, accelerations) instead of 2-tuple.

  • Breaking: LinearActuator.velocity attribute renamed to LinearActuator.speed to avoid conflict with the new Component.velocity property.

  • simulate_with_kinematics() now computes accelerations in addition to velocities.

[0.7.0] - 2025-12-13

Added in 0.7.0

  • Serialization: adds linkage serialization features.

  • Typing: adds typing.

  • Test: adds complete testing coverage.

  • Adds support for Python 3.14.

  • Hypergraph as the base theory for linkages.

Changed in 0.7.0

  • Switches to uv.

  • Renames HypostaticError to UnderconstrainedError and hyperstaticity() to indeterminacy(). Old names kept as deprecated aliases.

  • Separate linkage definition from actual solving:

    • The internal solver is now numba + NumPy, almost 100x faster!

    • The user-facing code is now based on Assur groups, that is more formal.

Fixed in 0.7.0

  • __find_solving_order__() is now properly tested and implemented (#16).

Deprecated in 0.7.0

  • Linear joint term is now deprecated in favor of Prismatic.

Removed in 0.7.0

  • Removed support for Python 3.9.

[0.6.0] - 2024-10-02

Added in 0.6.0

  • New joint: the Linear joint!

  • New sub-package: optimization.collections. optimization.collections.Agent and optimization.collections.MutableAgent are two new classes that should standardize the format of optimization, related to (#5).

    • Agent is immutable and inherits from a namedtuple. It is recommended to use it, as it is a bit faster.

    • MutableAgent is mutable. It may be deprecated/removed if Agent is satisfactory.

  • New sub-package: geometry.

    • It introduces two new functions line_from_points and circle_line_intersection.

  • New examples:

  • Linkage.set_completely is a new method combining both Linkage.set_num_constraints and Linkage.set_coords.

  • New exception NotCompletelyDefinedError, when a joint is reloading but its anchor coordinates are set to None.

  • Some run configuration files added for users of PyCharm:

    • Run all tests with “All Tests”.

    • Regenerate documentation with “Sphinx Documentation”.

Changed in 0.6.0

  • Optimization return type changed (#5):

    • trials_and_error_optimization return an array of MutableAgent.

    • particle_swarm_optimization return an array of one Agent.

    • It should not be a breaking change for most users.

  • Changes to the “history” style.

    • It is no longer a global variable in example scripts.

    • It was in format iterations[dimensions, score], now it is a standard iterations[score, dimensions, initial pos].

    • repr_polar_swarm (in example scripts) changed to follow the new format.

    • swarm_tiled_repr takes (index, swarm) as input argument. swarm is (score, dim, pos) for each agent for this iteration.

  • repr_polar_swarm reload frame only when a new buildable linkage is generated.

    • This makes the display much faster.

    • For each iteration, you may see linkages that do not exist anymore.

  • Folders reorganization:

    • The geometry module is now a package (pylinkage/geometry)

    • New package pylinkage/linkage:

      • pylinkage/linkage.py separated and inserted in this package.

    • New package: pylinkage/joints

      • Joints definition are in respective files.

    • New package pylinkage/optimization/

      • pylinkage/optimizer.py split and inserted in.

      • Trials-and-errors related functions goes to grid_search.py.

      • Particle swarm optimization is at particle_swarm.py.

      • New file utils.py for generate_bounds.

    • Tests follow the same renaming.

    • From the user perspective, no change (execution may be a bit faster)

    • source/ renamed to sphinx/ because it was confusing and only for Sphinx configuration.

  • Transition from Numpydoc to reST for docstrings (#12).

  • __secant_circles_intersections__ renamed to secant_circles_intersections (in pylinkage/geometry/secants.py).

Fixed in 0.6.0

  • swarm_tiled_repr in visualizer.py was wrongly assigning dimensions.

  • Setting locus_highlight in plot_static_linkage would result in an error.

  • Pivot.reload was returning arbitrary point when we had an infinity of solutions.

  • The highlighted locus was sometimes buggy in plot_static_linkage in visualizer.py.

Deprecated in 0.6.0

  • Using tqdm_verbosity is deprecated in favor of using disable=True in a tqdm object.

  • The Pivot class is deprecated in favor of the Revolute class. The name “Pivot joint” is not standard. Related to #13.

  • The hyperstaticity method is renamed indeterminacy in Linkage (linkage.py)

Removed in 0.6.0

  • Drops support for Python 3.7 and 3.8 as both versions reached end-of-life.

  • movement_bounding_bow is replaced by movement_bounding_box (typo in function name).

[0.5.3] - 2023-06-23

Added in 0.5.3

  • We now checked compatibility with Python 3.10 and 3.11.

  • pyproject.toml is now the official definition of the package.

  • Linkage.hyperstaticity now clearly outputs a warning when used.

Changed in 0.5.3

  • master branch is now main.

  • docs/example/fourbar_linkage.py can now be used as a module (not the target but anyway).

  • docs/examples moved to examples/ (main folder).

    • Now docs/ only contains sphinx documentation.

  • docs/examples/images moved to images/.

Fixed in 0.5.3

  • Setting a motor with a negative rotation angle do no longer break get_rotation_period (#7).

  • Pivot.reload and Linkage.__find_solving_order__ were raising Warnings (stopping the code), when they should only print a message (intended behavior).

  • Fixed many typos in documentation as well as in code.

  • The TestPSO.test_convergence is now faster on average, and when it fails in the first time, it launches a bigger test.

  • Minor linting in the demo file docs/example/fourbar_linkage.py.

Deprecated in 0.5.3

  • Using Python 3.7 is officially deprecated (end of life by 2023-06-27). It will no longer be tested, use it at your own risks!

[0.5.2] - 2021-07-21

Added in 0.5.2

  • You can see the best score and best dimensions updating in trials_and_errors_optimization.

Changed in 0.5.2

  • The optimizer tests are 5 times quicker (~1 second now) and raise less false positive.

  • The sidebar in the documentation makes navigation easier.

  • A bit of reorganization in optimizers, it should not affect users.

[0.5.1] - 2021-07-14

Added in 0.5.1

  • The trial and errors optimization now have a progress bar (same kind of the one in particle swarm optimization), using tqdm.

Changed in 0.5.1

[0.5.0] - 2021-07-12

End of alpha development! The package is now robust enough to be used by a mere human. This version introduces a lot of changes and simplifications, so everything is not perfect yet, but it is complete enough to be considered a beta version.

Git tags will no longer receive an “-alpha” mention.

Added in 0.5.0

  • It is now possible and advised to import useful functions from pylinkage.{object}, without full path. For instance, use from pylinkage import Linkage instead of from pylinkage.linkage import Linkage.

  • Each module had his header improved.

  • The generate_bounds functions is a simple way to generate bounds before optimization.

  • The order_relation arguments of particle_swarm_optimization and trials_and_errors_optimization let you choose between maximization and minimization problem.

  • You can specify a custom order relation with trials_and_errors_optimization.

  • The verbose argument in optimizers can disable verbosity.

  • Static joints can now be defined implicitly.

  • The utility module provides two useful decorators kinematic_minimization and kinematic_optimizatino. They greatly simplify the workflow of defining fitness functions.

  • Versioning is now done thanks to bump2version.

Changed in 0.5.0

  • The particle_swarm_optimization eval_func signature is now similar to the one ot trials_and_errors optimization. Wrappers are no longer needed!

  • The trials_and_errors_optimization function now asks for bounds instead of dilatation and compression factors.

  • In trials_and_errors_optimization absolute step delta_dim is now replaced by number of subdivisions divisions.

Fixed in 0.5.0

  • After many hours of computations, default parameters in particle_swarm_optimization are much more efficient. With the demo fourbar_linkage, the output wasn’t even convergent sometimes. Now we have a high convergence rate (~100%), and results equivalent to the trials_and_errors_optimization (in the example).

  • variator function of optimizer module was poorly working.

  • The docstrings were not displayed properly in documentation, this is fixed.

[0.4.1] - 2021-07-11

Added in 0.4.1

  • The legend in visualizer.py is back!

  • Documentation published to GitHub pages! It is contained in the docs/ folder.

  • setup.cfg now include links to the website.

Changed in 0.4.1

  • Examples moved from pylinkage/examples/ to docs/examples/.

  • Tests moved from pylinkage/tests/ to tests/.

[0.4.0] - 2021-07-06

Added in 0.4.0

  • The bounding_box method of geometry allows computing the bounding box of a 2D points finite set.

  • You can now customize colors of linkage’s bars with the COLOR_SWITCHER variable of visualizer.py.

  • movement_bounding_box in visualizer.py to get the bounding box of multiple loci.

  • parameters is optional in trials_and_errors_optimization (former exhaustive_optimization)

  • pylinkage/tests/test_optimizer.py for testing the optimizers, but it is a bit ugly as for now.

  • Flake8 validation in tox.ini

Fixed in 0.4.0

  • set_num_constraints in Linkage was misbehaving due to update 0.3.0.

  • Cost history is no longer plotted automatically after a PSO.

Changed in 0.4.0

  • exhaustive_optimization is now known as trials_and_errors_optimizattion.

  • Axes on linkage visualization are now named “x” and “y”. It was “Points abcsices” and “Ordinates”.

  • A default view of the linkage is displayed in plot_static_linkage.

  • Default padding in linkage representation was changed from an absolute value of 0.5 to a relative 20%.

  • Static view of linkage is now aligned with its kinematic one.

  • get_pos method of Linkage is now known as get_coords for consistency.

  • Parameters renamed, reorganized and removed in particle_swarm_optimization to align to PySwarms.

  • README.md updated consequently to the changes.

Removed in 0.4.0

  • Legacy built-in Particle Swarm Optimization, to avoid confusion.

  • We do no longer show a default legend on static representation.

[0.3.0] - 2021-07-05

Added in 0.3.0

  • Joint objects now have a get_constraints method, consistent with their set_constraints one.

  • Linkage now has a get_num_constraints method as syntactic sugar.

  • Code vulnerabilities checker

  • Walkthrough’s example has been expanded and now seems to be complete.

Changed in 0.3.0

  • Linkage’s method set_num_constraints behaviour changed! You should now add flat=False to come back to the previous behavior.

  • pylinkage/examples/fourbar_linkage.py expanded and finished.

  • The begin parameter of article_swarm_optimization is no longer mandatory. linkage.get_num_constraints() will be used if begin is not provided.

  • More flexible package version in environment.yml

  • Output file name now is formatted as “Kinematic {linkage.name}” in plot_kinematic_linkage function of pylinkage/visualizer.py

  • Python 3.6 is no longer tested in tox.ini. Python 3.9 is now tested.

Fixed in 0.3.0

  • When linkage animation was saved, last frames were often missing in pylinkage/visualizer.py, function plot_kinematic_linkage.

[0.2.2] - 2021-06-22

Added in 0.2.2

  • More continuous integration workflows for multiple Python versions.

Fixed in 0.2.2

  • README.md could not be seen in PyPi.

  • Various types

[0.2.1] - 2021-06-16

Added in 0.2.1

  • swarm_tiled_repr function for pylinkage/visualizer.py, for visualization of PySwarms.

  • EXPERIMENTAL! hyperstaticity method Linkage’s hyperstaticity (over constrained) calculation.

Changed in 0.2.1

  • pylinkage/exception.py now handles exceptions in another file.

  • Documentation improvements.

  • Python style improvements.

  • .gitignore now modified from the standard GitHub gitignore example for Python.

Fixed in 0.2.1

  • circle method of Pivot in pylinkage/linkage.py. It was causing errors

  • tox.ini now fixed.

[0.2.0] - 2021-06-14

Added in 0.2.0

  • pylinkage/vizualizer.py view your linkages using matplotlib!

  • Issue templates in .github/ISSUE_TEMPLATE/

  • .github/workflows/python-package-conda.yml: conda tests with unittest workflow.

  • CODE_OF_CONDUCT.md

  • MANIFEST.in

  • README.md

  • environment.yml

  • setup.cfg now replaces setup.py

  • tox.ini

  • CHANGELOG.md

Changed in 0.2.0

  • .gitignore Python Package specific extensions added

  • MIT LicenseLICENSE

  • lib/pylinkage/

  • tests/pylinkage/tests/

  • Revamped package organization.

  • Cleared setup.py

[0.0.1] - 2021-06-12

Added in 0.0.1

  • lib/geometry.py as a mathematical basis for kinematic optimization

  • lib/linkage.py, linkage builder

  • lib/optimizer.py, with Particle Swarm Optimization (built-in and PySwarms), and exhaustive optimization.

  • MIT License.

  • requirements.txt.

  • setup.py.

  • tests/__init__.py.

  • tests/test_geometry.py.

  • tests/test_linkage.py.

  • .gitignore.