Running mechanics

Under a root declaring time = Time.running() a simulation owns the machine’s coordinates and moves them by increments. This page states what that changes: what the run banks, how it takes requests, how it reads a law, where it stops, and what it refuses.

The bank

sim.state is the whole bank: every driver and every joint coordinate of the linked tree, including joints on leaves, site-declared joints and all six coordinates of a Free, by the qualified ids the document publishes. Plain ports and derived coordinates are not in it; they are calculations over the state, recomputed by the ordinary render on every tick. The initial bank is the untimed rest pose at the requested driver values, so Sim(machine, dt, state={'crank': 30.0}) starts where the relations put the machine at a crank of thirty. A joint coordinate the rest render leaves unbound is refused at construction, naming it: the run needs a rest value for every coordinate it keeps the history of, and the ordinary guarded rest default supplies one:

def simulate(self):
    if self.slide.travel.value is None:
        self.slide.travel = 4.0

An unconditional hand binding of a run-owned joint in simulate() raises DoublyBound at construction: that is a law written imperatively, and it belongs in a relation. Plain ports may still be fed in simulate() from owned coordinates, a spring’s height or a readout, but an imperatively fed port cannot source a compiled law into a banked coordinate. One tree has one run owner: constructing a new Sim over it releases the previous one.

Commands, not bindings

sim = Sim(machine, 0.1)

turn = sim.move('crank', by=10.0, duration=1.0)   # travel
sim.move('carriage', to=40.0, duration=0.5)       # land
spin = sim.rate('spindle', 90.0)                  # until released
sim.trigger('Advance')                            # an instruction

sim.run(1.0)
sim.rate('spindle', 0)

turn.status                     # 'completed'
turn.admitted                   # 10.0, in design units

Only a declared driver can be moved, by qualified id, and each has one owner at a time: a second move or rate on an owned input is refused naming both. Travel and destinations are design units; the bank stays in native units. duration is a whole number of ticks, zero included: a zero-duration move settles at the current tick. Every request returns a handle reporting status (active, completed, blocked, refused, cancelled), requested, admitted and remaining; a completed command leaves sim.commands the tick it finishes. handle.cancel() retires a command where it stands, and a replacement is accepted the same tick.

An instruction states either where its drivers land or how far they travel, Instruction({'crank': 40.0}, duration=0.5) or Instruction(by={'crank': 10.0}, duration=0.5), and both forms publish as buttons under a running root. sim.trigger(name) returns one handle per target input; an already-owned input refuses the instruction rather than replacing its move.

Controls on parts

A controls table beside instructions puts a request on the part itself: Button(part, instruction) is a press submitting that instruction; Turn(part, input) is a drag about the rotational coordinate the part rides and Slide(part, input) a drag along a translational one, each issued as relative moves on input. A control moves nothing itself and carries no state; ownership, admission, stops and outcomes are what trigger, move and rate state, and a blocked drag reports blocked with no hidden backlog.

part is a node, written as a relation’s path ends are; input is the Driver declaration, never an id string. The gesture’s axis and pivot are read off the tree, and per_unit, how far the part moves per unit of the input, is measured from the compiled program at the rest bank, never declared, because a number stated twice is a number that drifts. When a body has two freedoms, a crank that both lifts and turns, coordinate= names the joint declaration the gesture means; it may select a joint further up the part’s ancestry but never one on another branch. The framework never guesses which part a hand means: a dial moved by two things has no inferrable gesture, and the author declares it. Refusals name their facts where they exist: at class definition (a part the class does not hold, a repeated child, a drag over a driver the class does not declare), at compile (a part no run-owned coordinate poses, a Turn over a non-rotational coordinate, a drag whose input does not reach the coordinate, naming the inputs that do) and at publication (a drag that moves the part by nothing at rest). A control under a root that does not declare Time.running() is refused.

A subclass that declares its own controls table replaces the inherited one; to keep the base’s controls, spread them into it: controls = {**Base.controls, 'new': Turn(...)}. A kept control still works when the subclass replaces the child it names with a compatible declaration at the same path: its part and coordinate= are found on the replacement at compile, under the same checks. A new control that borrows another class’s child is refused.

Driving retained motion from time

The root’s running clock can be an explicit, read-only source of a relation. Given a joint-bearing Shaft, this machine advances six degrees per second with no driver and no startup command:

class Machine(AssemblyNode):
    time = Time.running()
    shaft = Shaft()
    time.drives(shaft.turn, ratio=6)

sim = Sim(Machine(), dt=0.02)
sim.run(2)
assert abs(sim.state['shaft.turn'] - 12) < 1e-9
assert sim.commands == ()

For an operating enable, name both sources in their written order:

def enabled_rotation(owners, target):
    return lambda seconds, enabled: 6 * seconds * (enabled > 0.5)

class Switchable(AssemblyNode):
    time = Time.running()
    enabled = Driver(default=1, range=(0, 1))
    shaft = Shaft()
    (time & enabled).drives(shaft.turn, law=enabled_rotation)

Turning the enable off holds the shaft; turning it on later resumes from the held position, because the change of gate branch is subtracted like any jump. This is an incremental position law, not a velocity callback: continuous pieces contribute f(end) - f(start), so a law seconds**2 stopped from time 1 to 3 resumes with an increment of 4**2 - 3**2 during the next second; it neither catches up on missed travel nor restarts a private clock. Declare mechanical phase as a joint coordinate when phase must be retained.

A physical stop clips the affected time-drive relation for the rest of that tick; all targets of that relation and its downstream train share the stop, an independent time-drive relation keeps going, and sim.time continues. The stopped relation retries on the next tick, with no backlog and no command. Time appears in neither the bank nor the command table: it cannot be moved, rated or targeted, and construction, inspection and publication advance nothing. self.time remains valid for an ordinary absolute transform, but assigning it to a run-owned joint in simulate() is a second binding and is refused; name time explicitly in the relation. Time supplies seconds, not gravity, torque or an escapement frequency; those remain the model’s own laws. A program with a time drive publishes document version 11, as does every new running export that carries neither a Follow relation nor profile contact.

One law, two readings

Laws are the same drives laws a posed machine uses, compiled once at construction by applying them to symbolic sources in the direction the rest pose solved each relation. Running, a law is integrated: a continuous law moves its driven coordinate by f(end) - f(start) from where it stood.

A law that jumps. A law may contain floor, ceil, sign, % or a comparison, and a periodic mechanism usually does. Over one tick the run cuts the path at every crossing of every jump surface it meets; on each piece every jump node holds one branch, read at the piece’s midpoint; and the increment is the sum of the branch-substituted law’s change over the pieces. So a pinion driven by a tooth window stands at 4 at rest, 76 after one crank turn and 148 after two, and the tick in which the crank passes 360 contributes exactly zero. Every crossing inside the tick is found, not only the difference of its ends. The five primitives cross where their level quantity reaches a surface: floor(x) and ceil(x) at every integer, sign(x) at zero, a % b over a / b at every nonzero integer, a comparison over a - b at zero. wrap is built on ceil and integrates through the same door; piecewise is a sum of clamp01 terms with no jump in it.

Disengagement is a law’s own business: a gate factor in a multi-source law, or a zero-slope region of a single-source one.

def clutch(sources, target):
    return lambda shaft, sleeve: -2 * shaft * (sleeve > 0.5)

(shaft.turn & sleeve.travel).drives(wheel.turn, law=clutch)

The wheel holds while the sleeve is out and takes the travel after engagement only on the tick the sleeve closes. A law naming several sources takes one straight path in their joint space. A gate does not block the input command; a joint range does.

Solved or searched. Where a followed quantity is affine in its sources, a sum, a constant multiple, a division by a constant, its crossings are solved by one division, exactly. Where it is piecewise affine, which is what abs, min, max and every profile built on them (clamp, clamp01, ramp, piecewise) make it, the path is cut at its kinks and each piece solved. Anything else, sin, sqrt, a product of two moving quantities, is sampled at 64 sub-intervals and each bracketed crossing bisected: correct, slower by tens of evaluations per crossing, and less exact by the bisection’s tolerance; a quantity that turns twice inside one sub-interval is outside the guarantee, and the answer is a smaller dt. A tick that would cross more than a thousand surfaces of one law is refused naming the relation, the coordinate, the primitive and the count. If a running machine is slower than you expect, this is where to look first: the same profile written with clamp01 costs almost nothing.

The run pays for deciding which part of a searched expression can change once per followed quantity per tick, not once per sample, so a law that reaches through a long chain of parts that stand still this tick is charged only for the part that moves.

A law that reads the coordinate it drives. Name the driven coordinate on both sides of one relation and the law reads it, taking the value the coordinate holds at the start of each piece:

GAP = 0.5   # the gap's half-width: the mechanism's clearance

def missing_tooth(sources, target):
    def law(ring, wheel):
        shifted = wheel + GAP
        return ring * (shifted - 360 * floor(shifted / 360) >= 2 * GAP)
    return law

(ring & wheel.turn).drives(wheel.turn, law=missing_tooth)

A dial swept from any digit runs to its gap and stops, the ring goes on sweeping past it, releasing the ring keeps the partial clearing, and sweeping an already-cleared dial does not turn it. The rules: the read must be a switch (with every jump replaced by its branch the law must no longer name the coordinate; a bare % still carries the slope and is refused); the relation drives exactly one coordinate, a joint the run banks; it binds nothing at rest, so the joint’s rest value is the author’s guarded default; and it needs a run. State the disengaged region as a band with the mechanism’s own width, entered from either side: after a cut the coordinate is committed at the nearest representable value on the far side of the surface, so a dial standing in its gap reads the same branch on every later tick and survives a snapshot bit for bit. A gate whose disengaged state is a single value has no width and is not promised to hold. A cut of such a law is a crossing, never a stop. A document carrying one is version 6.

A selection decides which sources a law reads. A machine whose dependencies are selected by where one of its own parts stands (a Curta’s fixed carry lever, tripped by whichever dial the carriage brought under it) has a cyclic union of dependencies. The selection is the comparison the model already writes, a term multiplied by a gate on the selecting coordinate, and a cycle every selection breaks is a block: one entry of the program, ordered once per piece of a tick. The block’s selectors are located first, the stretch cut at their surfaces, and on each piece the branches read at the midpoint decide which dependencies are active and in what order the members run. A selection change alone moves nothing; a block relation binds nothing at rest; a piece that still cannot be ordered refuses the tick naming the piece, the branches and the relations on the cycle. Determined sources retain their actual stroke, dwell and landing timing through the block and through ordinary chains; additional selector cuts do not replace that motion by endpoint ramps. New running documents, including blocks, declare version 11, 12 when they carry a Follow relation, or 13 when a Bound uses profile contact.

Retained clearance. Play(low, high) is the narrow running law for a follower separated from its source by backlash or clearance: it holds inside the interval, is collected at either flank, and is released when the source reverses. Each play source is a run-owned driver or the output of another play edge in one linear chain; anything else is refused. A new document carrying one is version 11.

Two moving surfaces. Follow(lower=, upper=) is the narrower law for a retained coordinate that rests freely between two independently moving, authored boundaries, a ball between a bell and a collar: either surface pushes it only on contact, and it stays where a retreating surface left it, max(lower, min(retained, upper)) at every certified piece of the tick. Its two sources are inputs, held bank coordinates or unbranched affine chains; the lower and upper Bound relations on the same coordinate must state the same two surfaces, and they locate the first contact at which the surfaces become incompatible, checking the certified cuts as well as the uniform samples. Other ancestry, a curved boundary or a contact too narrow to represent is refused, not approximated; the follower feeds no other relation. A document carrying one is version 12.

Finite profile contact. ConvexProfile, imported with profile_overlap from machinome.simulation.profile, holds authored finite convex planar loops. profile_overlap rotates and translates two profiles in XY and returns 1.0 when they touch, inclusively, and 0.0 otherwise, so a running Bound can use it as a term in its limit expression: an axial stop that applies only while a pinion’s profile meets a drum’s. The Bound samples and attributes the stop exactly as it does any other constraint; anywhere else a symbolic call is refused. The answer is pointwise: contact between two samples is not detected, and nothing checks that the loops cover the installed parts, which is the model’s own proof. A document using it is version 13.

A range is a physical stop

Under a running root a joint’s declared range is a mechanical limit, not a refusal of the tick. When a tick would take a banked coordinate outside a bound, the run locates the fraction of the tick at which it reaches that bound, commits it there exactly, and the tick commits like any other. What stops with it is the connected group: every input whose own movement over that stretch pushes the stopped coordinate, and everything those inputs alone determine. An input that does not reach it, or reaches it only through a law that is currently disengaged, runs its full tick. The tick becomes two segments, each integrated by the procedure above, and the earliest stop is always taken first. A command whose input is stopped is retired blocked with the travel it actually admitted, fractional within the tick, and never resumes: the caller issues a new command.

A bound given as a callable of the joint’s own coordinate is compiled once and evaluated once per tick from the committed bank, so within a tick the bound is a number: that is the ratchet of 8. A machine with history, admitting the same travel whether a reverse is taken in one tick or forty. A Bound that reads other coordinates is a constraint evaluated along the tick’s path: whenever a read moves, it is sampled at 64 fractions and bisected, and the stop blocks every input whose motion carries the constraint outward, through the bounded coordinate or through what it reads. That is how an interlock stops the key’s withdrawal without moving the plug.

A constraint an ancestor adds to a nested joint (Constraints from an ancestor) is the same kind of stop. Its own coordinate stays frozen at the value committed when the tick began while its reads follow the attempted motion, so a moving read can never overrun a standing target. Which inputs the stop blocks is judged at the contact, not by comparing the level at the two ends of the whole request: each candidate input is tried alone across the located obstruction, and it is blocked if its motion carries the level outward there. A periodic lockout therefore stops a long crank request at its first closing contact even when the requested endpoint lies in a later open window, with no hidden splitting of the request and no cap of one turn. The remainder of a blocked request is discarded; relief does not resume it, and the caller issues a new command. Under a time drive only the pushing relation stops, and elapsed time and unrelated drives continue. Two limits remain. A forbidden interval that fits entirely between two of the 64 samples can be missed. And an obstruction that needs several inputs together, none of which pushes alone, is refused transactionally rather than resolved.

Where the threshold a coordinate lands on is itself moving, because another part carries it, the landing is oriented by the two parts’ relative motion, not by the driven part’s own direction; a threshold that overtakes a part travelling the other way is still found. Two parts that follow one another exactly, one carrying the other’s contact level, are recognised as such by an exact check of the affine law between them, so a rounding residue of a few ulps is not mistaken for a departure. Neither adds a tolerance: a nonzero relative motion, however small, is real.

The stop’s fraction is exact wherever every edge between the pushing inputs and the stopped coordinate is affine or piecewise affine; for a curved upstream edge the stopped coordinate is still committed at its bound exactly while the group’s other coordinates stop on the linearized path. A coordinate that leaves its range and returns within one tick is not stopped; it cannot happen where the determiner is affine, and elsewhere the answer is a smaller dt. A Driver’s own range is untouched by all of this: it is presentation, and a machine driven past its declared travel is a crash a simulation must be able to show.

Snapshot, restore, reset, record

sim.snapshot() is a value: the bank, the tick, dt, the active commands and the program’s identity. sim.restore(snapshot) puts the run back, refusing a snapshot over a different machine or dt; sim.reset() restores sim.initial. Recording is explicit and bounded: record=None keeps nothing, record=64 keeps rings of the most recent sixty-four ticks in sim.trajectory, of located crossings in sim.crossings (the relation, the coordinate, the primitive, the surface and the fraction of the tick) and of stops in sim.stops (the coordinate, which bound, its evaluated value, the fraction and the inputs blocked). every() sees each tick as it happens without a recording.

What a running root refuses

Each by name: a law written over Python’s math rather than machinome.math; a relation into a banked coordinate whose source is a plain port an author’s simulate() binds; an author’s simulate() that binds a run-owned coordinate unconditionally; a law that could only ever jump (floor(turns) alone), because every jump is subtracted; a jumping law whose driven ends are all intermediates, which cannot retain a history; a law that reads the coordinate it drives continuously; a root driver named like a joint anywhere in the tree (turn beside plug.turn); a repeat() child that owns a joint, because drivers-0 is not a legal id. A refused tick commits nothing, and the commands that moved an input in it retire refused.

sim.stops entries name the blocked driver ids in inputs and, in a separate time_drives tuple, the blocked time-drive relations.

What it publishes

A running root’s document declares version 11, 12 when its program carries a Follow edge, or 13 when a Bound uses profile contact (the profile data then travels once as program.profiles), and carries a program beside the geometry: the coordinate table with each bank id’s kind, rest value, unit and domain, the compiled edges in program order with their expressions and jump plans, the candidate table of which inputs reach what, the program identity and the clock name. Every joint’s placement is published as its own coordinate’s id, so a consumer poses the geometry from a bank it committed rather than from the law read absolutely, which is the whole difference between a machine that accumulates and one that snaps back. The model’s self.time publishes as the free name time bound to elapsed simulation seconds. A tree that declares controls carries a controls table beside instructions, additively. A program with time-driven relations lists them as program.time_drives, each naming the edge that reads time. Source-timing semantics participate in the program identity: endpoint-era snapshots refuse restore into newly compiled programs before changing state. Re-export with the corrected producer and use a viewer supporting its version; do not lower the document version manually. Posed, looping and clocked version selection is unchanged. What a build publishes has the version table.