Files
create/docs/src/solver/expression-dag.md
forbes acc255972d
Some checks failed
Build and Test / build (pull_request) Failing after 7m52s
feat(assembly): fixed reference planes + solver docs
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
2026-02-21 09:09:16 -06:00

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 dictionary
  • diff(var) -- return a new Expr tree for the partial derivative with respect to var
  • simplify() -- 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) becomes Const(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^2 becomes x * x (avoids pow() 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

  1. Parameter registration. ParamTable.add("Part001/tx", 10.0) creates a Var("Part001/tx") node and records its current value.

  2. Constraint building. Constraint classes compose Var nodes with arithmetic to produce residual Expr trees. For example, CoincidentConstraint builds body_i.world_point() - body_j.world_point(), producing 3 residual expressions.

  3. 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.

  4. 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.