Skip to content

Phase 3a-2 — LocalNewtonLoweringPass: in-function scalar Newton iteration #75

Description

@petlenz

Phase 3a-2 — the second half of Phase 3a (#34): emit the actual local Newton iteration body. Builds directly on Phase 2.2 (TimeIntegrationPass, residual) + Phase 3a-1 (LocalJacobianPass, Jacobian). Scoped to a single scalar state variable; multi-equation coupled Newton is explicitly out (needs LocalNewtonSystem IR — a later slice).

Goal

For a recipe with a scalar evolution equation, the generated compute function should solve for the converged state variable in-function via Newton iteration, instead of exposing <sv>_residual / <sv>_jacobian as outputs.

Decision (confirmed): in-function loop that REPLACES the residual/Jacobian outputs. The state variable becomes the out-parameter; R and J become loop-local temporaries.

void Hardening_compute(
    double const K, double const alpha_old,
    double const dt, double &alpha_out) {
  double alpha = alpha_old;            // initial guess = previous step
  for (int it = 0; it < max_iter; ++it) {
    auto t0 = K * alpha;               // loop-local CSE (recomputed each iter)
    double R = (alpha - alpha_old) / dt - t0;
    double J = 1.0 / dt - K;
    if (std::abs(R) < tol) break;
    alpha -= R / J;
  }
  alpha_out = alpha;                   // converged state
}

Why the infrastructure is lighter than feared

render_compute_function is flat (all CSE temps via ctx.render_statements(), then out = rhs;). A Newton loop needs the residual/Jacobian temps emitted INSIDE the loop (they depend on the changing iterate). The hook already exists: CodeGenContext::reset() clears statements but KEEPS the temp counter (so re-rendered blocks can't collide), and render_statements(indent) accepts an indent. A render_newton_segment(...) helper can: reset the context, re-emit the residual+Jacobian via the emit pipeline, render_statements(" ") indented into the loop body. No CodeGenContext refactor required — this likely fits one PR.

Deliverables

  1. LocalNewtonLoweringPass (passes/local_newton_lowering_pass.h), registered AFTER LocalJacobianPass.
    • Precondition: pass_tags::jacobian_emitted (+ the state_variable_lowering_inputs bundle).
    • Postcondition: new pass_tags::newton_lowered.
    • For each scalar evolution equation: records the residual + Jacobian expressions for a Newton segment and marks the state variable as a Newton unknown (so the renderer emits the loop, not R/J outputs).
  2. Newton-segment emission in the render path:
    • render_newton_segment helper (in passes/internal/, same bottom-include pattern as backward_euler.h) producing the double <sv> = <sv>_old; for(...){...} <sv>_out = <sv>; block.
    • Loop-local CSE via CodeGenContext::reset() + indented render_statements.
    • Configurable tol (default 1e-10) + max_iter (default 50) — surfaced as NewtonOptions on the pass or the evolution equation; default-constructed for now.
  3. Phase 2.2: render_compute_function Category-aware branch for StateVariable symbols #60StateVariableCurrent → out-parameter. In the Newton path the state variable's current symbol is emitted as double &<sv>_out, not double const <sv>. render_compute_function's param_decl/output_decl gain a Category-aware branch (this is exactly issue Phase 2.2: render_compute_function Category-aware branch for StateVariable symbols #60). The <sv>_residual/<sv>_jacobian outputs from TIP/LJP are suppressed when a Newton segment owns that state variable.
  4. Pipeline wiring in emit_compute_function(): register LocalNewtonLoweringPass after LJP, gated on !m_evolution_equations.empty().
  5. Tests:
    • LocalNewtonTest.cpp — pass precondition graph (requires jacobian_emitted); segment structure (loop present, R/J are locals not out-params, state var is double &..._out); residual/Jacobian outputs suppressed.
    • Numerical end-to-end via the compile_check_driver harness: emit a linear-hardening Newton recipe, call it, assert the converged alpha matches the analytic fixed point alpha* = alpha_old / (1 - K*dt) within 1e-10. This IS the Phase 1.3 (Phase 1.3 — 1D scalar plasticity de-risk spike (Phase 3 risk reduction) #32) de-risk spike, finally realised.
  6. Docs: docs/workflow.md sequence diagram + §5 table (add LNLP, mark 3a-2 landed).

Explicit out-of-scope (later slices)

Open design decisions (resolve during implementation)

  • NewtonOptions placement — on the pass (uniform per recipe) vs per evolution equation (per-SV tol/max-iter). Recommend pass-level for 3a-2; per-equation when a real consumer needs it.
  • Control-flow emit generality — a contained render_newton_segment helper (recommended for one consumer) vs a general statement-IR (EmitStmt = assign | loop | if). Defer the statement-IR until Phase 3b's tangent emission becomes a second control-flow consumer.

Dependencies / refs

Acceptance

  • A linear-hardening recipe emits a self-contained Newton-solving function; compile_check_driver confirms numerical convergence to the analytic fixed point within 1e-10.
  • <sv>_residual / <sv>_jacobian no longer appear as outputs for a Newton-owned state variable; the state variable appears as double &<sv>_out.
  • All existing tests stay green; MooseBackendDoesNotYetWireStateVariables_KNOWN_GAP unaffected (still Phase 2.6).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions