Assembly Origin Planes: - AssemblyObject::setupObject() relabels origin planes to Top (XY), Front (XZ), Right (YZ) on assembly creation - CommandCreateAssembly.py makes origin planes visible by default - AssemblyUtils.cpp getObjFromRef() resolves LocalCoordinateSystem to child datum elements for joint references to origin planes - TestAssemblyOriginPlanes.py: 9 integration tests covering structure, labels, grounding, reference resolution, solver, and save/load round-trip Solver Documentation: - docs/src/solver/: 7 new pages covering architecture overview, expression DAG, constraints, solving algorithms, diagnostics, assembly integration, and writing custom solvers - docs/src/SUMMARY.md: added Kindred Solver section
4.4 KiB
Expression DAG
The expression DAG is the foundation of the Kindred solver. All constraint equations, Jacobian entries, and residuals are built as immutable trees of Expr nodes. This lets the solver compute exact symbolic derivatives and simplify constant sub-expressions before the iterative solve loop.
Source: mods/solver/kindred_solver/expr.py
Node types
Every node is a subclass of Expr and implements three methods:
eval(env)-- evaluate the expression given a name-to-value dictionarydiff(var)-- return a new Expr tree for the partial derivative with respect tovarsimplify()-- return an algebraically simplified copy
Leaf nodes
| Node | Description | diff(x) |
|---|---|---|
Const(v) |
Literal floating-point value | 0 |
Var(name) |
Named parameter (from ParamTable) |
1 if name matches, else 0 |
Unary nodes
| Node | Description | diff(x) |
|---|---|---|
Neg(f) |
Negation: -f |
-f' |
Sin(f) |
Sine: sin(f) |
cos(f) * f' |
Cos(f) |
Cosine: cos(f) |
-sin(f) * f' |
Sqrt(f) |
Square root: sqrt(f) |
f' / (2 * sqrt(f)) |
Binary nodes
| Node | Description | diff(x) |
|---|---|---|
Add(a, b) |
Sum: a + b |
a' + b' |
Sub(a, b) |
Difference: a - b |
a' - b' |
Mul(a, b) |
Product: a * b |
a'b + ab' (product rule) |
Div(a, b) |
Quotient: a / b |
(a'b - ab') / b^2 (quotient rule) |
Pow(a, n) |
Power: a^n (constant exponent only) |
n * a^(n-1) * a' |
Sentinels
ZERO = Const(0.0) and ONE = Const(1.0) are pre-allocated constants used by diff() to avoid allocating trivial nodes.
Operator overloading
Python's arithmetic operators are overloaded on Expr, so constraints can be written in natural notation:
from kindred_solver.expr import Var, Const
x = Var("x")
y = Var("y")
# Build the expression: x^2 + 2*x*y - 1
expr = x**2 + 2*x*y - Const(1.0)
# Evaluate at x=3, y=4
expr.eval({"x": 3.0, "y": 4.0}) # 32.0
# Symbolic derivative w.r.t. x
dx = expr.diff("x").simplify() # 2*x + 2*y
dx.eval({"x": 3.0, "y": 4.0}) # 14.0
The _wrap() helper coerces plain int and float values to Const nodes automatically, so 2 * x works without wrapping the 2.
Simplification
simplify() applies algebraic identities bottom-up:
- Constant folding:
Const(2) + Const(3)becomesConst(5) - Identity elimination:
x + 0 = x,x * 1 = x,x^0 = 1,x^1 = x - Zero propagation:
0 * x = 0 - Negation collapse:
-(-x) = x - Power expansion:
x^2becomesx * x(avoidspow()in evaluation)
Simplification is applied once to each Jacobian entry after symbolic differentiation, before the solve loop begins. This reduces the expression tree size and speeds up repeated evaluation.
How the solver uses expressions
-
Parameter registration.
ParamTable.add("Part001/tx", 10.0)creates aVar("Part001/tx")node and records its current value. -
Constraint building. Constraint classes compose
Varnodes with arithmetic to produce residualExprtrees. For example,CoincidentConstraintbuildsbody_i.world_point() - body_j.world_point(), producing 3 residual expressions. -
Jacobian construction. Newton-Raphson calls
r.diff(name).simplify()for every (residual, free parameter) pair to build the symbolic Jacobian. This happens once before the solve loop. -
Evaluation. Each Newton iteration calls
expr.eval(env)on every residual and Jacobian entry using the current parameter snapshot.eval()is a simple recursive tree walk with dictionary lookups.
Design notes
Why not numpy directly? Symbolic expressions give exact derivatives without finite-difference approximations, and enable pre-passes (substitution, single-equation solve) that can eliminate variables before the iterative solver runs. The overhead of tree evaluation is acceptable for the problem sizes encountered in assembly solving (typically tens to hundreds of variables).
Why immutable? Immutability means diff() can safely share sub-tree references between the original and derivative expressions. It also simplifies the substitution pass, which rebuilds trees with Const nodes replacing fixed Var nodes.
Limitations. Pow differentiation only supports constant exponents. Variable exponents would require logarithmic differentiation (d/dx f^g = f^g * (g' * ln(f) + g * f'/f)), which hasn't been needed for assembly constraints.