Merge pull request 'docs(solver): update drag protocol docs to reflect implemented caching' (#330) from docs/solver-drag-cache into main
Reviewed-on: #330
This commit was merged in pull request #330.
This commit is contained in:
@@ -98,53 +98,107 @@ if hasattr(FreeCADGui, "ActiveDocument"):
|
||||
|
||||
## Interactive drag protocol
|
||||
|
||||
The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol:
|
||||
The drag protocol provides real-time constraint solving during viewport part dragging. It is a three-phase protocol with a caching layer that avoids rebuilding the constraint system on every mouse move.
|
||||
|
||||
### pre_drag(ctx, drag_parts)
|
||||
|
||||
Called when the user begins dragging. Stores the context and dragged part IDs, then runs a full solve to establish the starting state.
|
||||
Called when the user begins dragging. Builds the constraint system once, runs the substitution pre-pass, constructs the symbolic Jacobian, compiles the evaluator, performs an initial solve, and caches everything in a `_DragCache` for reuse across subsequent `drag_step()` calls.
|
||||
|
||||
```python
|
||||
def pre_drag(self, ctx, drag_parts):
|
||||
self._drag_ctx = ctx
|
||||
self._drag_parts = set(drag_parts)
|
||||
return self.solve(ctx)
|
||||
|
||||
system = _build_system(ctx)
|
||||
|
||||
half_spaces = compute_half_spaces(...)
|
||||
weight_vec = build_weight_vector(system.params)
|
||||
|
||||
residuals = substitution_pass(system.all_residuals, system.params)
|
||||
# single_equation_pass is intentionally skipped — it bakes variable
|
||||
# values as constants that become stale when dragged parts move.
|
||||
|
||||
jac_exprs = [[r.diff(name).simplify() for name in free] for r in residuals]
|
||||
compiled_eval = try_compile_system(residuals, jac_exprs, ...)
|
||||
|
||||
# Initial solve (Newton-Raphson + BFGS fallback)
|
||||
newton_solve(residuals, system.params, ...)
|
||||
|
||||
# Cache for drag_step() reuse
|
||||
cache = _DragCache()
|
||||
cache.system = system
|
||||
cache.residuals = residuals
|
||||
cache.jac_exprs = jac_exprs
|
||||
cache.compiled_eval = compiled_eval
|
||||
cache.half_spaces = half_spaces
|
||||
cache.weight_vec = weight_vec
|
||||
...
|
||||
return result
|
||||
```
|
||||
|
||||
**Important:** `single_equation_pass` is not used in the drag path. It analytically solves single-variable equations and bakes the results as `Const()` nodes into downstream expressions. During drag, those baked values become stale when part positions change, causing constraints to silently stop being enforced. Only `substitution_pass` (which replaces genuinely grounded parameters) is safe to cache.
|
||||
|
||||
### drag_step(drag_placements)
|
||||
|
||||
Called on each mouse move. Updates the dragged parts' placements in the stored context, then re-solves. Since the parts moved only slightly from the previous position, Newton-Raphson converges in 1-2 iterations.
|
||||
Called on each mouse move. Updates only the dragged part's 7 parameter values in the cached `ParamTable`, then re-solves using the cached residuals, Jacobian, and compiled evaluator. No system rebuild occurs.
|
||||
|
||||
```python
|
||||
def drag_step(self, drag_placements):
|
||||
ctx = self._drag_ctx
|
||||
cache = self._drag_cache
|
||||
params = cache.system.params
|
||||
|
||||
# Update only the dragged part's parameters
|
||||
for pr in drag_placements:
|
||||
for part in ctx.parts:
|
||||
if part.id == pr.id:
|
||||
part.placement = pr.placement
|
||||
break
|
||||
return self.solve(ctx)
|
||||
pfx = pr.id + "/"
|
||||
params.set_value(pfx + "tx", pr.placement.position[0])
|
||||
params.set_value(pfx + "ty", pr.placement.position[1])
|
||||
params.set_value(pfx + "tz", pr.placement.position[2])
|
||||
params.set_value(pfx + "qw", pr.placement.quaternion[0])
|
||||
params.set_value(pfx + "qx", pr.placement.quaternion[1])
|
||||
params.set_value(pfx + "qy", pr.placement.quaternion[2])
|
||||
params.set_value(pfx + "qz", pr.placement.quaternion[3])
|
||||
|
||||
# Solve with cached artifacts — no rebuild
|
||||
newton_solve(cache.residuals, params, ...,
|
||||
jac_exprs=cache.jac_exprs,
|
||||
compiled_eval=cache.compiled_eval)
|
||||
|
||||
return result
|
||||
```
|
||||
|
||||
### post_drag()
|
||||
|
||||
Called when the drag ends. Clears the stored state.
|
||||
Called when the drag ends. Clears the cached state.
|
||||
|
||||
```python
|
||||
def post_drag(self):
|
||||
self._drag_ctx = None
|
||||
self._drag_parts = None
|
||||
self._drag_cache = None
|
||||
```
|
||||
|
||||
### Performance notes
|
||||
### _DragCache
|
||||
|
||||
The current implementation re-solves from scratch on each drag step, using the updated placements as the initial guess. This is correct and simple. For assemblies with fewer than ~50 parts, interactive frame rates are maintained because:
|
||||
The cache holds all artifacts built in `pre_drag()` that are invariant across drag steps (constraint topology doesn't change during a drag):
|
||||
|
||||
| Field | Contents |
|
||||
|-------|----------|
|
||||
| `system` | `_System` -- owns `ParamTable` and `Expr` trees |
|
||||
| `residuals` | `list[Expr]` -- after substitution pass |
|
||||
| `jac_exprs` | `list[list[Expr]]` -- symbolic Jacobian |
|
||||
| `compiled_eval` | `Callable` or `None` -- native compiled evaluator |
|
||||
| `half_spaces` | `list[HalfSpace]` -- branch trackers |
|
||||
| `weight_vec` | `ndarray` or `None` -- minimum-movement weights |
|
||||
| `post_step_fn` | `Callable` or `None` -- half-space correction callback |
|
||||
|
||||
### Performance
|
||||
|
||||
The caching layer eliminates the expensive per-frame overhead (~150 ms for system build + Jacobian construction + compilation). Each `drag_step()` only evaluates the cached expressions at updated parameter values:
|
||||
|
||||
- Newton-Raphson converges in 1-2 iterations from a nearby initial guess
|
||||
- Pre-passes eliminate fixed parameters before the iterative loop
|
||||
- The symbolic Jacobian is recomputed each step (no caching yet)
|
||||
|
||||
For larger assemblies, cached incremental solving (reusing the decomposition and Jacobian structure across drag steps) is planned as a future optimization.
|
||||
- The compiled evaluator (`codegen.py`) uses native Python `exec` for flat evaluation, avoiding the recursive tree-walk overhead
|
||||
- The substitution pass compiles grounded-body parameters to constants, reducing the effective system size
|
||||
- DOF counting is skipped during drag for speed (`result.dof = -1`)
|
||||
|
||||
## Diagnostics integration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user