diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ece800..b579788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ the [Deprecations](https://hugofara.github.io/pylinkage/deprecations.html) page. ## [Unreleased] +## [1.3.0] - 2026-10-25 + ### Added - **Python 3.15 support.** It is tested in CI. On 3.15 the `cad` extra @@ -17,9 +19,12 @@ the [Deprecations](https://hugofara.github.io/pylinkage/deprecations.html) page. ### Changed -- **Breaking:** Dropped Python 3.10 support, which reached end of life in - October 2026. Minimum version is now Python 3.11. `typing_extensions` is no - longer a dependency. +- **Python 3.11 or later is required.** Python 3.10 reached end of life in + October 2026. pip keeps a 3.10 environment on 1.2.2. `typing_extensions` is + no longer a dependency. +- The development lockfile is refreshed. It moves pillow to 13.0.0, along with + certifi, tornado, urllib3 and nbconvert, past the advisories Dependabot + reported. None of them reach the published package. ### Fixed diff --git a/CITATION.cff b/CITATION.cff index 26bf532..535b173 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -7,8 +7,8 @@ authors: given-names: Hugo orcid: "https://orcid.org/0009-0001-0769-4434" affiliation: "Independent Researcher, France" -version: 1.2.2 -date-released: "2026-09-18" +version: 1.3.0 +date-released: "2026-10-25" identifiers: - type: doi value: "10.5281/zenodo.21207486" diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 6b54cf4..2372992 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '1.2.2', + VERSION: '1.3.0', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/api/modules.html b/docs/api/modules.html index 2a08533..f402cab 100644 --- a/docs/api/modules.html +++ b/docs/api/modules.html @@ -6,14 +6,14 @@ -
>>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> arc_crank = ArcCrank(
... anchor=O1,
... radius=1.0,
@@ -263,6 +263,17 @@ Submodules
+
+set_coord(x: float | None, y: float | None) None
+Place the output, and read the crank angle from where it lands.
+The angle is the direction from the anchor to (x, y), brought
+onto the arc; the sweep restarts in the positive direction, as it
+does on a new crank. Like a Crank, the arc crank is then
+entirely in the state its coordinates describe, so putting a
+linkage back in a position puts its actuators back too.
+
+
@@ -290,7 +301,7 @@ SubmodulesExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> crank.position
(1.0, 0.0)
@@ -415,7 +426,7 @@ SubmodulesExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> actuator = LinearActuator(anchor=O1, angle=0.0, stroke=2.0, speed=0.1)
>>> actuator.position
(0.0, 0.0)
@@ -505,6 +516,18 @@ Submodules
+
+set_coord(x: float | None, y: float | None) None
+Place the output, and read the extension from where it lands.
+The extension is the projection of (x, y) from the anchor
+onto the actuator’s axis, held within [0, stroke]; the stroke
+restarts in the extending direction, as it does on a new
+actuator. The actuator is then entirely in the state its
+coordinates describe, so putting a linkage back in a position
+puts its actuators back too.
+
+
-
speed: float
@@ -535,7 +558,7 @@ Module contentsfrom pylinkage.components import Ground
from pylinkage.actuators import Crank
-O1 = Ground(0.0, 0.0, name="O1")
+O1 = Ground(0.0, 0.0, name="O1")
crank = Crank(anchor=O1, radius=1.0)
@@ -544,7 +567,7 @@ Module contentsfrom pylinkage.actuators import ArcCrank
import math
-O1 = Ground(0.0, 0.0, name="O1")
+O1 = Ground(0.0, 0.0, name="O1")
arc_crank = ArcCrank(anchor=O1, radius=1.0, arc_start=0, arc_end=math.pi/2)
@@ -552,7 +575,7 @@ Module contentsfrom pylinkage.components import Ground
from pylinkage.actuators import LinearActuator
-O1 = Ground(0.0, 0.0, name="O1")
+O1 = Ground(0.0, 0.0, name="O1")
actuator = LinearActuator(anchor=O1, angle=0.0, stroke=2.0, speed=0.1)
@@ -578,7 +601,7 @@ Module contentsExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> arc_crank = ArcCrank(
... anchor=O1,
... radius=1.0,
@@ -687,6 +710,17 @@ Module contents
+
+set_coord(x: float | None, y: float | None) None
+Place the output, and read the crank angle from where it lands.
+The angle is the direction from the anchor to (x, y), brought
+onto the arc; the sweep restarts in the positive direction, as it
+does on a new crank. Like a Crank, the arc crank is then
+entirely in the state its coordinates describe, so putting a
+linkage back in a position puts its actuators back too.
+
+
@@ -708,7 +742,7 @@ Module contentsExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> crank = Crank(anchor=O1, radius=1.0)
>>> crank.position
(1.0, 0.0)
@@ -816,7 +850,7 @@ Module contentsExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> actuator = LinearActuator(anchor=O1, angle=0.0, stroke=2.0, speed=0.1)
>>> actuator.position
(0.0, 0.0)
@@ -906,6 +940,18 @@ Module contents
+-
+set_coord(x: float | None, y: float | None) None
+Place the output, and read the extension from where it lands.
+The extension is the projection of (x, y) from the anchor
+onto the actuator’s axis, held within [0, stroke]; the stroke
+restarts in the extending direction, as it does on a new
+actuator. The actuator is then entirely in the state its
+coordinates describe, so putting a linkage back in a position
+puts its actuators back too.
+
+
-
speed: float
diff --git a/docs/api/pylinkage.assur.html b/docs/api/pylinkage.assur.html
index f7eb9d1..d54ab5e 100644
--- a/docs/api/pylinkage.assur.html
+++ b/docs/api/pylinkage.assur.html
@@ -6,14 +6,14 @@
- pylinkage.assur package — pylinkage 1.2.2 documentation
+ pylinkage.assur package — pylinkage 1.3.0 documentation
-
+
@@ -307,8 +307,8 @@ Submodules>>> assur = AssurMechanism(mechanism)
>>>
>>> # Access formal properties
->>> print(f"DOF: {assur.degree_of_freedom}")
->>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
+>>> print(f"DOF: {assur.degree_of_freedom}")
+>>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
>>>
>>> # Simulation still works via delegation
>>> for positions in assur.step():
@@ -553,10 +553,10 @@ SubmodulesExample
>>> result = decompose_assur_groups(graph)
->>> print(f"Ground: {result.ground}")
->>> print(f"Drivers: {result.drivers}")
+>>> print(f"Ground: {result.ground}")
+>>> print(f"Drivers: {result.drivers}")
>>> for group in result.groups:
-... print(f" {group.joint_signature}: {group.internal_nodes}")
+... print(f" {group.joint_signature}: {group.internal_nodes}")
@@ -630,10 +630,10 @@ SubmodulesExample
->>> graph = LinkageGraph(name="Four-bar")
+>>> graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> result = decompose_assur_groups(graph)
->>> print(f"Found {len(result.groups)} Assur groups")
+>>> print(f"Found {len(result.groups)} Assur groups")
@@ -681,7 +681,7 @@ SubmodulesExample
->>> edge = Edge("AB", source="A", target="B")
+>>> edge = Edge("AB", source="A", target="B")
@@ -753,12 +753,12 @@ SubmodulesExample
->>> graph = LinkageGraph(name="Four-bar")
->>> graph.add_node(Node("A", role=NodeRole.GROUND))
->>> graph.add_node(Node("B", role=NodeRole.DRIVER))
->>> graph.add_edge(Edge("AB", source="A", target="B"))
->>> graph.neighbors("A")
-['B']
+>>> graph = LinkageGraph(name="Four-bar")
+>>> graph.add_node(Node("A", role=NodeRole.GROUND))
+>>> graph.add_node(Node("B", role=NodeRole.DRIVER))
+>>> graph.add_edge(Edge("AB", source="A", target="B"))
+>>> graph.neighbors("A")
+['B']
@@ -947,7 +947,7 @@ SubmodulesExample
->>> node = Node("A", role=NodeRole.GROUND)
+>>> node = Node("A", role=NodeRole.GROUND)
>>> node.joint_type
<JointType.REVOLUTE: 1>
@@ -1336,7 +1336,7 @@ SubmodulesExample
->>> hg = HypergraphLinkage(name="Four-bar")
+>>> hg = HypergraphLinkage(name="Four-bar")
>>> # ... add nodes and edges ...
>>> assur_graph = from_hypergraph(hg)
@@ -1360,7 +1360,7 @@ SubmodulesExample
->>> assur_graph = LinkageGraph(name="Four-bar")
+>>> assur_graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> hg = to_hypergraph(assur_graph)
@@ -1407,9 +1407,9 @@ SubmodulesExample
->>> graph = LinkageGraph(name="Four-bar")
->>> graph.add_node(Node("A", role=NodeRole.GROUND))
->>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
+>>> graph = LinkageGraph(name="Four-bar")
+>>> graph.add_node(Node("A", role=NodeRole.GROUND))
+>>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
>>> mechanism = graph_to_mechanism(graph, dims)
>>> for positions in mechanism.step():
... print(positions)
@@ -1507,8 +1507,8 @@ SubmodulesExample
->>> graph_to_json(graph, "my_linkage.json")
->>> restored = graph_from_json("my_linkage.json")
+>>> graph_to_json(graph, "my_linkage.json")
+>>> restored = graph_from_json("my_linkage.json")
@@ -1547,8 +1547,8 @@ SubmodulesExample
->>> graph_to_json(graph, "my_linkage.json")
->>> restored = graph_from_json("my_linkage.json")
+>>> graph_to_json(graph, "my_linkage.json")
+>>> restored = graph_from_json("my_linkage.json")
@@ -1595,14 +1595,14 @@ Submodules>>> from pylinkage.assur import parse_signature, signature_to_hypergraph
>>>
>>> # Parse formal syntax
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> print(sig.joint_signature)
-'RRR'
+'RRR'
>>>
>>> # Generate pure topology
>>> graph = signature_to_hypergraph(sig)
>>> print(list(graph.nodes.keys()))
-['anchor_0', 'anchor_1', 'internal_0']
+['anchor_0', 'anchor_1', 'internal_0']
@@ -1652,13 +1652,13 @@ SubmodulesExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig.group_class
<AssurGroupClass.DYAD: 1>
>>> sig.canonical_form
-'RRR'
+'RRR'
@@ -1721,12 +1721,12 @@ SubmodulesExample
->>> isomer_to_canonical("RT_R")
-'RPR'
->>> isomer_to_canonical("T_R_T")
-'PRP'
->>> isomer_to_canonical("RRR")
-'RRR'
+>>> isomer_to_canonical("RT_R")
+'RPR'
+>>> isomer_to_canonical("T_R_T")
+'PRP'
+>>> isomer_to_canonical("RRR")
+'RRR'
@@ -1753,16 +1753,16 @@ SubmodulesExample
->>> sig, roles = parse_isomer_signature("RT_R")
+>>> sig, roles = parse_isomer_signature("RT_R")
>>> sig
-'RT_R'
+'RT_R'
>>> roles
-('revolute', 'slider', 'guide', 'revolute')
+('revolute', 'slider', 'guide', 'revolute')
->>> sig, roles = parse_isomer_signature("T_R_T")
+>>> sig, roles = parse_isomer_signature("T_R_T")
>>> roles
-('slider', 'guide', 'revolute', 'guide', 'slider')
+('slider', 'guide', 'revolute', 'guide', 'slider')
@@ -1788,19 +1788,19 @@ SubmodulesExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
->>> sig = parse_signature("R_P_R") # With separators
+>>> sig = parse_signature("R_P_R") # With separators
>>> sig.canonical_form
-'RPR'
+'RPR'
->>> sig = parse_signature("RTR") # T accepted as alias for P
+>>> sig = parse_signature("RTR") # T accepted as alias for P
>>> sig.canonical_form
-'RPR'
+'RPR'
@@ -1820,15 +1820,15 @@ SubmodulesExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> cls = signature_to_group_class(sig)
>>> cls.__name__
-'DyadRRR'
+'DyadRRR'
->>> cls = signature_to_group_class("RPR")
+>>> cls = signature_to_group_class("RPR")
>>> cls.__name__
-'DyadRPR'
+'DyadRPR'
@@ -1859,7 +1859,7 @@ SubmodulesExample
->>> graph = signature_to_hypergraph("RRR")
+>>> graph = signature_to_hypergraph("RRR")
>>> len(graph.nodes)
3 # 2 anchors + 1 internal
>>> len(graph.edges)
@@ -1867,8 +1867,8 @@ Submodules>>> # With prefix for unique IDs
->>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
->>> "leg1_anchor_0" in leg.nodes
+>>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
+>>> "leg1_anchor_0" in leg.nodes
True
@@ -1900,8 +1900,8 @@ Module contents>>> assur = AssurMechanism(mechanism)
>>>
>>> # Access structural properties
->>> print(f"DOF: {assur.degree_of_freedom}")
->>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
+>>> print(f"DOF: {assur.degree_of_freedom}")
+>>> print(f"Groups: {[g.joint_signature for g in assur.assur_groups]}")
>>>
>>> # Simulation via delegation
>>> for positions in assur.step():
@@ -2252,13 +2252,13 @@ Module contentsExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
>>> sig.group_class
<AssurGroupClass.DYAD: 1>
>>> sig.canonical_form
-'RRR'
+'RRR'
@@ -2325,10 +2325,10 @@ Module contentsExample
>>> result = decompose_assur_groups(graph)
->>> print(f"Ground: {result.ground}")
->>> print(f"Drivers: {result.drivers}")
+>>> print(f"Ground: {result.ground}")
+>>> print(f"Drivers: {result.drivers}")
>>> for group in result.groups:
-... print(f" {group.joint_signature}: {group.internal_nodes}")
+... print(f" {group.joint_signature}: {group.internal_nodes}")
@@ -2512,7 +2512,7 @@ Module contentsExample
->>> edge = Edge("AB", source="A", target="B")
+>>> edge = Edge("AB", source="A", target="B")
@@ -2629,12 +2629,12 @@ Module contentsExample
->>> graph = LinkageGraph(name="Four-bar")
->>> graph.add_node(Node("A", role=NodeRole.GROUND))
->>> graph.add_node(Node("B", role=NodeRole.DRIVER))
->>> graph.add_edge(Edge("AB", source="A", target="B"))
->>> graph.neighbors("A")
-['B']
+>>> graph = LinkageGraph(name="Four-bar")
+>>> graph.add_node(Node("A", role=NodeRole.GROUND))
+>>> graph.add_node(Node("B", role=NodeRole.DRIVER))
+>>> graph.add_edge(Edge("AB", source="A", target="B"))
+>>> graph.neighbors("A")
+['B']
@@ -2823,7 +2823,7 @@ Module contentsExample
->>> node = Node("A", role=NodeRole.GROUND)
+>>> node = Node("A", role=NodeRole.GROUND)
>>> node.joint_type
<JointType.REVOLUTE: 1>
@@ -2973,10 +2973,10 @@ Module contentsExample
->>> graph = LinkageGraph(name="Four-bar")
+>>> graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> result = decompose_assur_groups(graph)
->>> print(f"Found {len(result.groups)} Assur groups")
+>>> print(f"Found {len(result.groups)} Assur groups")
@@ -3000,7 +3000,7 @@ Module contentsExample
->>> hg = HypergraphLinkage(name="Four-bar")
+>>> hg = HypergraphLinkage(name="Four-bar")
>>> # ... add nodes and edges ...
>>> assur_graph = from_hypergraph(hg)
@@ -3039,8 +3039,8 @@ Module contentsExample
->>> graph_to_json(graph, "my_linkage.json")
->>> restored = graph_from_json("my_linkage.json")
+>>> graph_to_json(graph, "my_linkage.json")
+>>> restored = graph_from_json("my_linkage.json")
@@ -3079,8 +3079,8 @@ Module contentsExample
->>> graph_to_json(graph, "my_linkage.json")
->>> restored = graph_from_json("my_linkage.json")
+>>> graph_to_json(graph, "my_linkage.json")
+>>> restored = graph_from_json("my_linkage.json")
@@ -3114,9 +3114,9 @@ Module contentsExample
->>> graph = LinkageGraph(name="Four-bar")
->>> graph.add_node(Node("A", role=NodeRole.GROUND))
->>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
+>>> graph = LinkageGraph(name="Four-bar")
+>>> graph.add_node(Node("A", role=NodeRole.GROUND))
+>>> dims = Dimensions(node_positions={"A": (0, 0)}, ...)
>>> mechanism = graph_to_mechanism(graph, dims)
>>> for positions in mechanism.step():
... print(positions)
@@ -3167,19 +3167,19 @@ Module contentsExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> sig.joints
(JointType.REVOLUTE, JointType.REVOLUTE, JointType.REVOLUTE)
->>> sig = parse_signature("R_P_R") # With separators
+>>> sig = parse_signature("R_P_R") # With separators
>>> sig.canonical_form
-'RPR'
+'RPR'
->>> sig = parse_signature("RTR") # T accepted as alias for P
+>>> sig = parse_signature("RTR") # T accepted as alias for P
>>> sig.canonical_form
-'RPR'
+'RPR'
@@ -3199,15 +3199,15 @@ Module contentsExample
->>> sig = parse_signature("RRR")
+>>> sig = parse_signature("RRR")
>>> cls = signature_to_group_class(sig)
>>> cls.__name__
-'DyadRRR'
+'DyadRRR'
->>> cls = signature_to_group_class("RPR")
+>>> cls = signature_to_group_class("RPR")
>>> cls.__name__
-'DyadRPR'
+'DyadRPR'
@@ -3238,7 +3238,7 @@ Module contentsExample
->>> graph = signature_to_hypergraph("RRR")
+>>> graph = signature_to_hypergraph("RRR")
>>> len(graph.nodes)
3 # 2 anchors + 1 internal
>>> len(graph.edges)
@@ -3246,8 +3246,8 @@ Module contents>>> # With prefix for unique IDs
->>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
->>> "leg1_anchor_0" in leg.nodes
+>>> leg = signature_to_hypergraph("RRR", prefix="leg1_")
+>>> "leg1_anchor_0" in leg.nodes
True
@@ -3270,7 +3270,7 @@ Module contentsExample
->>> assur_graph = LinkageGraph(name="Four-bar")
+>>> assur_graph = LinkageGraph(name="Four-bar")
>>> # ... add nodes and edges ...
>>> hg = to_hypergraph(assur_graph)
diff --git a/docs/api/pylinkage.bridge.html b/docs/api/pylinkage.bridge.html
index 7e2c8cc..7c2e5c9 100644
--- a/docs/api/pylinkage.bridge.html
+++ b/docs/api/pylinkage.bridge.html
@@ -6,14 +6,14 @@
- pylinkage.bridge package — pylinkage 1.2.2 documentation
+ pylinkage.bridge package — pylinkage 1.3.0 documentation
-
+
diff --git a/docs/api/pylinkage.cam.html b/docs/api/pylinkage.cam.html
index f286566..1b66f83 100644
--- a/docs/api/pylinkage.cam.html
+++ b/docs/api/pylinkage.cam.html
@@ -6,14 +6,14 @@
- pylinkage.cam package — pylinkage 1.2.2 documentation
+ pylinkage.cam package — pylinkage 1.3.0 documentation
-
+
diff --git a/docs/api/pylinkage.components.html b/docs/api/pylinkage.components.html
index e69e9ed..89b0cac 100644
--- a/docs/api/pylinkage.components.html
+++ b/docs/api/pylinkage.components.html
@@ -6,14 +6,14 @@
- pylinkage.components package — pylinkage 1.2.2 documentation
+ pylinkage.components package — pylinkage 1.3.0 documentation
-
+
@@ -141,8 +141,8 @@ SubmodulesExample
->>> O1 = Ground(0.0, 0.0, name="O1")
->>> O2 = Ground(2.0, 0.0, name="O2")
+>>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O2 = Ground(2.0, 0.0, name="O2")
>>> O1.position
(0.0, 0.0)
@@ -201,16 +201,16 @@ SubmodulesExample
->>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O1 = Ground(0.0, 0.0, name="O1")
>>> crank = Crank(anchor=O1, radius=1.0)
->>> O2 = Ground(2.0, 0.0, name="O2")
+>>> O2 = Ground(2.0, 0.0, name="O2")
>>> # Track a point at 45 degrees from crank->O2 line
>>> tracker = PointTracker(
... anchor1=crank.output,
... anchor2=O2,
... distance=0.5,
... angle=math.pi/4,
-... name="tracer_point"
+... name="tracer_point"
... )
@@ -433,8 +433,8 @@ Module contentsExample
->>> O1 = Ground(0.0, 0.0, name="O1")
->>> O2 = Ground(2.0, 0.0, name="O2")
+>>> O1 = Ground(0.0, 0.0, name="O1")
+>>> O2 = Ground(2.0, 0.0, name="O2")
>>> O1.position
(0.0, 0.0)
@@ -486,16 +486,16 @@ Module contentsExample
-