Some checks failed
Build and Test / build (pull_request) Failing after 7m52s
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
97 lines
4.4 KiB
Markdown
97 lines
4.4 KiB
Markdown
# 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.
|