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