Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,42 @@ All notable changes to pylinkage are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
What the version number promises, and how a name is retired, is in
the [Deprecations](https://hugofara.github.io/pylinkage/deprecations.html) page.

## [Unreleased]

### 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`.

- **`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).

- **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
Expand Down Expand Up @@ -46,6 +77,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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
Expand Down Expand Up @@ -77,6 +123,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,24 @@ show_linkage(my_linkage)

![A four-bar linkage animated](https://github.com/HugoFara/pylinkage/raw/main/docs/assets/Kinematic%20My%20four-bar%20linkage.gif)

### Which API should I use?

The one above. `components` (frame points), `actuators` (motor inputs),
`dyads` (the constrained pairs that close the loops) and `simulation.Linkage`
(the container) are *the* way to define a mechanism, and every tutorial starts
there. The same names are importable straight from `pylinkage` as a shortcut.

Pylinkage has other representations, each with a job:

| Representation | Use it when |
|---|---|
| `mechanism.MechanismBuilder` | you think in link lengths first, joints second (below) |
| `hypergraph` | you compose mechanisms from reusable sub-assemblies, or need a topology (not geometry) to reason about |
| `assur` | you want the structural decomposition of a mechanism into Assur groups |
| `mechanism.Mechanism` | you need the low-level links-and-joints model everything else lowers to |

They all convert to and from each other; none is needed to get started.

### Alternative: Links-First Builder

For a more mechanical engineering-oriented approach, use `MechanismBuilder` to define links with their lengths first, then connect them:
Expand Down Expand Up @@ -207,6 +225,14 @@ Level 4: Applications → Optimization, Synthesis, Symbolic, Visualization

</details>

## Stability

Pylinkage follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
What that covers, and how a name is retired, is on one page:
[Deprecations](https://hugofara.github.io/pylinkage/deprecations.html).
The public surface is pinned in
[`tests/test_public_api.py`](https://github.com/HugoFara/pylinkage/blob/main/tests/test_public_api.py).

## Requirements

- Python >= 3.10
Expand Down
12 changes: 12 additions & 0 deletions docs/source/api/modules.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
pylinkage
=========

The packages below are listed alphabetically. To define and simulate a
mechanism you need four of them, in this order: :mod:`pylinkage.components`
(frame points), :mod:`pylinkage.actuators` (motor inputs),
:mod:`pylinkage.dyads` (the constrained pairs that close the loops) and
:mod:`pylinkage.simulation` (the ``Linkage`` container). The
:doc:`getting started tutorial </tutorials/getting_started>` walks through
them; the rest are what you reach for afterwards.

A name is public when it appears in a package's ``__all__``; modules inside a
package are implementation. See :doc:`/deprecations` for the rule and for
every name currently on its way out.

.. toctree::
:maxdepth: 4

Expand Down
77 changes: 77 additions & 0 deletions docs/source/deprecations.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

Names that still work but are on their way out, with what to use instead.

## What is public

A name is public when it is listed in the `__all__` of a package, and a
package is public when it is reachable as an attribute of `pylinkage`
(`pylinkage.synthesis`, `pylinkage.optimization.collections`, …). That is the
whole rule. The policy below applies to those names and nothing else.

Two consequences:

- **Modules inside a package are implementation.** `from
pylinkage.synthesis.path_generation import path_generation` works today and
may stop working without notice; `from pylinkage.synthesis import
path_generation` is the promise. Newer modules carry a leading underscore
to make this visible (`population._member`, `synthesis._types`); older ones
will be renamed as they are touched.
- **Each name has one home.** `Ground` lives in `pylinkage.components`, not
also in `pylinkage.dyads`. The top-level package re-exports the definition
path (`Ground`, `Crank`, `RRRDyad`, `Linkage`, …) for convenience; every
other second location is a deprecated alias, listed below.

The surface is pinned name by name in `tests/test_public_api.py`, so it cannot
change without a diff in that file.

Some public names are **provisional**: exported and documented, but their
shape may still change in a minor release. They are the newer subsystems —
`pylinkage.topology`, `pylinkage.solver`, the co-design family in
`pylinkage.optimization` (`co_optimize`, `CoOptimizationConfig`, …), the
multi-topology synthesis in `pylinkage.synthesis` (`NBarSolution`,
`generalized_synthesis`, …), hierarchical composition in
`pylinkage.hypergraph`, and the CAD export in `pylinkage.visualizer`. A change
to one is announced in the changelog, not through a warning.

## How deprecation works here

pylinkage follows [semantic versioning](https://semver.org/spec/v2.0.0.html).
Expand Down Expand Up @@ -33,8 +65,32 @@ whether an upgrade will affect you.
| `pylinkage.dyads.Dyad` | `pylinkage.components.Component` | 2.0.0 |
| `pylinkage.dyads.ConnectedDyad` | `pylinkage.components.ConnectedComponent` | 2.0.0 |
| `pylinkage.synthesis.Dyad` | `pylinkage.synthesis.BurmesterDyad` | 2.0.0 |
| `pylinkage.dyads.Ground` | `pylinkage.components.Ground` | 2.0.0 |
| `pylinkage.dyads.PointTracker` | `pylinkage.components.PointTracker` | 2.0.0 |
| `pylinkage.dyads.Component` | `pylinkage.components.Component` | 2.0.0 |
| `pylinkage.dyads.ConnectedComponent` | `pylinkage.components.ConnectedComponent` | 2.0.0 |
| `pylinkage.dyads.Crank` | `pylinkage.actuators.Crank` | 2.0.0 |
| `pylinkage.dyads.ArcCrank` | `pylinkage.actuators.ArcCrank` | 2.0.0 |
| `pylinkage.dyads.LinearActuator` | `pylinkage.actuators.LinearActuator` | 2.0.0 |
| `pylinkage.dyads.Linkage` | `pylinkage.simulation.Linkage` | 2.0.0 |
| `pylinkage.optimization.Ensemble` | `pylinkage.population.Ensemble` | 2.0.0 |
| `pylinkage.assur.MobilityResult` | `pylinkage.topology.MobilityInfo` | 2.0.0 |
| `pylinkage.assur.StructuralAnalysis` | `pylinkage.topology.compute_mobility()` | 2.0.0 |
| `path_generation(n_orientation_samples=...)` | `orientation_resolution=` | 2.0.0 |

### Why the `dyads` re-exports are going away

`pylinkage.dyads` used to re-export the frame, actuator and container classes
"for convenience", so `pylinkage.dyads.Ground` and `pylinkage.components.Ground`
were both correct and tutorials disagreed on which to use. Under the one-home
rule above, `dyads` defines dyads. The objects are unchanged.

### Why `MobilityResult` and `StructuralAnalysis` are going away

They were exported from `pylinkage.assur` with no function anywhere in the
package that produced them. The mobility analysis that exists is
`pylinkage.topology.compute_mobility()`, which returns a `MobilityInfo`.

### Why the `Dyad` names are going away

Three unrelated classes were reachable as `Dyad`, and only two of them were
Expand Down Expand Up @@ -89,6 +145,27 @@ from pylinkage.components import Component, ConnectedComponent
from pylinkage.synthesis import BurmesterDyad
```

```python
# Before
from pylinkage.dyads import Ground, Crank, RRRDyad, Linkage

# After -- one home per name, or the top-level shortcut
from pylinkage.components import Ground
from pylinkage.actuators import Crank
from pylinkage.dyads import RRRDyad
from pylinkage.simulation import Linkage

from pylinkage import Ground, Crank, RRRDyad, Linkage # equivalent
```

```python
# Before
from pylinkage.optimization import Ensemble

# After
from pylinkage.population import Ensemble
```

```python
# Before
path_generation(points, n_orientation_samples=36)
Expand Down
4 changes: 3 additions & 1 deletion docs/source/tutorials/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ Core Concepts

Pylinkage models planar linkage mechanisms with three kinds of objects from
the ``components`` / ``actuators`` / ``dyads`` packages, plus a thin
container:
container. This is the recommended way to define a mechanism; the other
representations (``mechanism``, ``hypergraph``, ``assur``) are covered in
later tutorials and convert to and from this one.

1. **Components**: building blocks of the mechanism (``Ground`` anchors,
``Crank`` / ``LinearActuator`` drivers, and dyads such as ``RRRDyad``,
Expand Down
77 changes: 67 additions & 10 deletions src/pylinkage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,36 @@
"""

__all__ = [
# Assur group module
# Subpackages. Heavy or optional ones load on first access.
"actuators",
"assur",
# Dyads module (user-facing API for mechanism building)
"cam",
"components",
"dimensions",
"dyads",
# Mechanism module (new Links + Joints model)
"exceptions",
"geometry",
"hypergraph",
"linkage",
"mechanism",
# Symbolic computation module (lazy, requires sympy)
"optimization",
"population",
"simulation",
"solver",
"symbolic",
# Synthesis module (lazy, requires scipy)
"synthesis",
"topology",
"visualizer",
# The definition path: frame, actuators, dyads, container
"Ground",
"PointTracker",
"Crank",
"ArcCrank",
"LinearActuator",
"RRRDyad",
"RRPDyad",
"PPDyad",
"FixedDyad",
# Canonical types (from _types.py)
"JointType",
"NodeRole",
Expand Down Expand Up @@ -67,16 +87,22 @@
]

import importlib as _importlib
from typing import TYPE_CHECKING

# --- Eager imports (lightweight, always available) ---
# Assur group module for graph-based linkage representation
from . import actuators as actuators
from . import assur as assur

# Dyads module - user-facing API for mechanism building
from . import cam as cam
from . import components as components
from . import dimensions as dimensions
from . import dyads as dyads

# Mechanism module - new Links + Joints model
from . import exceptions as exceptions
from . import geometry as geometry
from . import hypergraph as hypergraph
from . import linkage as linkage
from . import mechanism as mechanism
from . import simulation as simulation
from . import topology as topology
from ._simulation_context import (
Simulation as Simulation,
)
Expand All @@ -103,6 +129,15 @@
from ._types import (
PortId as PortId,
)
from .actuators import ArcCrank as ArcCrank
from .actuators import Crank as Crank
from .actuators import LinearActuator as LinearActuator
from .components import Ground as Ground
from .components import PointTracker as PointTracker
from .dyads import FixedDyad as FixedDyad
from .dyads import PPDyad as PPDyad
from .dyads import RRPDyad as RRPDyad
from .dyads import RRRDyad as RRRDyad
from .exceptions import (
NotCompletelyDefinedError as NotCompletelyDefinedError,
)
Expand Down Expand Up @@ -148,10 +183,32 @@

# --- Lazy imports (heavy optional dependencies) ---

if TYPE_CHECKING:
# Eager imports for type checkers only; at runtime these load on first access.
from . import optimization, population, solver, symbolic, synthesis, visualizer
from .optimization import (
collections,
generate_bounds,
kinematic_maximization,
kinematic_minimization,
particle_swarm_optimization,
trials_and_errors_optimization,
)
from .population import Ensemble, Member, Population
from .visualizer import (
plot_kinematic_linkage,
plot_static_linkage,
show_linkage,
swarm_tiled_repr,
)

_LAZY_SUBMODULES = {
"optimization",
"population",
"solver",
"symbolic",
"synthesis",
"visualizer",
}

_LAZY_ATTRS: dict[str, str] = {
Expand Down
1 change: 0 additions & 1 deletion src/pylinkage/actuators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,5 @@
__all__ = [
"ArcCrank",
"Crank",
"DEFAULT_ANGULAR_VELOCITY",
"LinearActuator",
]
Loading
Loading