Skip to main content

echidna_optim/
piggyback.rs

1use std::fmt;
2
3use echidna::{BytecodeTape, Dual, Float};
4
5/// Reason a piggyback solve failed to converge.
6///
7/// Marked `#[non_exhaustive]` so future variants can be added without
8/// breaking exhaustive `match`es. Numeric fields use `f64` (cast via
9/// `Float::to_f64`) for uniform diagnostic output regardless of the
10/// solver's `F` type.
11#[non_exhaustive]
12#[derive(Debug, Clone)]
13pub enum PiggybackError {
14    /// The primal `z_{k+1} = G(z_k, x)` produced a non-finite norm
15    /// (relative-norm `||z_new - z||/(1 + ||z||)` is NaN/Inf), or
16    /// the primal vector itself contained non-finite components in
17    /// the forward-adjoint loop. `last_norm` is the primal-delta
18    /// relative norm at the detecting iteration: non-finite when
19    /// detection came from the norm check (the usual case);
20    /// finite — and itself diagnostic — when detection came from
21    /// the componentwise finite check (primal vector overflowed
22    /// mid-iteration while the step-to-step delta stayed bounded).
23    PrimalDivergence { iteration: usize, last_norm: f64 },
24    /// Primal stayed finite but the tangent
25    /// `ż_{k+1} = G_z · ż_k + G_x · ẋ` produced non-finite values.
26    /// Catches the ratio-converging case where the primal norm
27    /// remains bounded while individual tangent components overflow.
28    /// `last_norm` is the primal-delta relative norm at the
29    /// detecting iteration — **finite** by construction here (the
30    /// tangent-only divergence path takes the norm-finite branch
31    /// before the componentwise check fires); surfacing it tells
32    /// the caller the primal iteration was bounded while the JVP
33    /// overflowed.
34    TangentDivergence { iteration: usize, last_norm: f64 },
35    /// Adjoint `λ_{k+1} = G_z^T · λ_k + z̄` produced non-finite
36    /// values (norm or individual components). `last_norm` is the
37    /// adjoint-delta relative norm at the detecting iteration:
38    /// non-finite when detection came from the norm check; finite
39    /// when it came from the componentwise `lambda_new` check.
40    AdjointDivergence { iteration: usize, last_norm: f64 },
41    /// `piggyback_tangent_solve` reached `max_iter` before **both** the
42    /// primal and tangent step-deltas met `tol`. `z_norm` is the final
43    /// iteration's relative primal-delta norm
44    /// (`||z_new - z|| / (1 + ||z||)`) — a value just over `tol` signals
45    /// proximity to convergence; many orders over signals stagnation; a
46    /// value **at or below** `tol` means the primal had converged and the
47    /// tangent (which starts at zero and converges on its own schedule)
48    /// was the stream still iterating. `iteration` equals `max_iter`.
49    IterationsExhaustedTangent { iteration: usize, z_norm: f64 },
50    /// `piggyback_adjoint_solve` reached `max_iter` without meeting
51    /// `tol`. `lam_norm` is the final iteration's relative adjoint-
52    /// delta norm (`||λ_new - λ|| / (1 + ||λ||)`).
53    IterationsExhaustedAdjoint { iteration: usize, lam_norm: f64 },
54    /// `piggyback_forward_adjoint_solve` reached `max_iter` without
55    /// meeting `tol` on both norms simultaneously. Each field is the
56    /// final iteration's relative norm for the corresponding stream.
57    IterationsExhaustedForwardAdjoint {
58        iteration: usize,
59        z_norm: f64,
60        lam_norm: f64,
61    },
62    /// A runtime-supplied vector argument to a public `*_solve` fn
63    /// had an unexpected length. `field` names the argument (e.g.
64    /// `"z_dot"`, `"z_bar"`), `expected` is the length the API
65    /// requires (typically `num_states` or `x.len()`), `actual` is
66    /// the length the caller supplied.
67    ///
68    /// Note: tape-shape contract mismatches
69    /// (`validate_step_tape`) and step-fn argument mismatches
70    /// (`piggyback_tangent_step[_with_buf]`) continue to panic —
71    /// those are programmer-contract violations, not recoverable
72    /// runtime failures.
73    DimensionMismatch {
74        field: &'static str,
75        expected: usize,
76        actual: usize,
77    },
78}
79
80impl fmt::Display for PiggybackError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            PiggybackError::PrimalDivergence {
84                iteration,
85                last_norm,
86            } => {
87                write!(
88                    f,
89                    "piggyback: primal diverged at iteration {iteration} (last_norm = {last_norm:.3e})"
90                )
91            }
92            PiggybackError::TangentDivergence {
93                iteration,
94                last_norm,
95            } => {
96                write!(
97                    f,
98                    "piggyback: tangent diverged at iteration {iteration} (last_norm = {last_norm:.3e})"
99                )
100            }
101            PiggybackError::AdjointDivergence {
102                iteration,
103                last_norm,
104            } => {
105                write!(
106                    f,
107                    "piggyback: adjoint diverged at iteration {iteration} (last_norm = {last_norm:.3e})"
108                )
109            }
110            PiggybackError::IterationsExhaustedTangent { iteration, z_norm } => {
111                write!(
112                    f,
113                    "piggyback: tangent solve reached max_iter = {iteration} (z_norm = {z_norm:.3e})"
114                )
115            }
116            PiggybackError::IterationsExhaustedAdjoint {
117                iteration,
118                lam_norm,
119            } => {
120                write!(
121                    f,
122                    "piggyback: adjoint solve reached max_iter = {iteration} (lam_norm = {lam_norm:.3e})"
123                )
124            }
125            PiggybackError::IterationsExhaustedForwardAdjoint {
126                iteration,
127                z_norm,
128                lam_norm,
129            } => {
130                write!(
131                    f,
132                    "piggyback: forward-adjoint solve reached max_iter = {iteration} (z_norm = {z_norm:.3e}, lam_norm = {lam_norm:.3e})"
133                )
134            }
135            PiggybackError::DimensionMismatch {
136                field,
137                expected,
138                actual,
139            } => {
140                write!(
141                    f,
142                    "piggyback: dimension mismatch for `{field}` (expected {expected}, got {actual})"
143                )
144            }
145        }
146    }
147}
148
149impl std::error::Error for PiggybackError {}
150
151echidna::assert_send_sync!(PiggybackError);
152
153/// Validate that a step tape G: R^(m+n) -> R^m has the expected shape.
154///
155/// Uses `assert_eq!` (panic) rather than `Result` because shape
156/// mismatches are programmer errors — calling `piggyback_*_solve` with
157/// an inconsistent tape is a contract violation, not a runtime
158/// numerical failure that callers should recover from.
159fn validate_step_tape<F: Float>(tape: &BytecodeTape<F>, z: &[F], x: &[F], num_states: usize) {
160    assert_eq!(z.len(), num_states);
161    assert_eq!(tape.num_inputs(), num_states + x.len());
162    assert_eq!(
163        tape.num_outputs(),
164        num_states,
165        "step tape must have num_outputs == num_states (G: R^(m+n) -> R^m)"
166    );
167}
168
169/// One tangent piggyback step through a fixed-point map G.
170///
171/// Given the iteration `z_{k+1} = G(z_k, x)`, computes both the primal step
172/// and the tangent propagation `ż_{k+1} = G_z · ż_k + G_x · ẋ` in a single
173/// forward pass using dual numbers.
174///
175/// Returns `(z_new, z_dot_new)`.
176pub fn piggyback_tangent_step<F: Float>(
177    step_tape: &BytecodeTape<F>,
178    z: &[F],
179    x: &[F],
180    z_dot: &[F],
181    x_dot: &[F],
182    num_states: usize,
183) -> (Vec<F>, Vec<F>) {
184    let mut buf = Vec::new();
185    piggyback_tangent_step_with_buf(step_tape, z, x, z_dot, x_dot, num_states, &mut buf)
186}
187
188/// One tangent piggyback step, reusing `buf` across calls.
189///
190/// Same as [`piggyback_tangent_step`] but avoids reallocating the internal
191/// dual-number buffer on each call.
192pub fn piggyback_tangent_step_with_buf<F: Float>(
193    step_tape: &BytecodeTape<F>,
194    z: &[F],
195    x: &[F],
196    z_dot: &[F],
197    x_dot: &[F],
198    num_states: usize,
199    buf: &mut Vec<Dual<F>>,
200) -> (Vec<F>, Vec<F>) {
201    validate_step_tape(step_tape, z, x, num_states);
202    let m = num_states;
203    let n = x.len();
204    assert_eq!(z_dot.len(), m, "z_dot length must equal num_states");
205    assert_eq!(x_dot.len(), n, "x_dot length must equal x length");
206
207    // Build dual inputs: [Dual(z_i, ż_i), ..., Dual(x_j, ẋ_j), ...]
208    let mut dual_inputs = Vec::with_capacity(m + n);
209    for i in 0..m {
210        dual_inputs.push(Dual::new(z[i], z_dot[i]));
211    }
212    for j in 0..n {
213        dual_inputs.push(Dual::new(x[j], x_dot[j]));
214    }
215
216    // Dual-specialized sweep: custom ops evaluate their tangents at the
217    // current (z, x) via eval_dual instead of a recording-time linearization.
218    step_tape.forward_tangent_dual(&dual_inputs, buf);
219
220    // Extract outputs: .re -> z_new, .eps -> z_dot_new
221    let out_indices = step_tape.all_output_indices();
222    let mut z_new = Vec::with_capacity(m);
223    let mut z_dot_new = Vec::with_capacity(m);
224    for &idx in out_indices {
225        let d = buf[idx as usize];
226        z_new.push(d.re);
227        z_dot_new.push(d.eps);
228    }
229
230    (z_new, z_dot_new)
231}
232
233/// Tangent piggyback solve: find fixed point z* = G(z*, x) and its tangent ż*.
234///
235/// Iterates the fixed-point map `z_{k+1} = G(z_k, x)` while simultaneously
236/// propagating tangents `ż_{k+1} = G_z · ż_k + G_x · ẋ`.
237///
238/// Returns `Ok((z_star, z_dot_star, iterations))` when **both** iterates
239/// have converged (relative step-delta below `tol` for each). The tangent
240/// starts at zero and converges on its own schedule — its error decays as
241/// `ρᵏ·‖ż*‖` regardless of how close `z0` is to `z*` — so primal
242/// convergence alone would return a truncated Neumann sum for a
243/// warm-started primal. Returns
244/// `Err(PiggybackError::PrimalDivergence)` when the primal norm becomes
245/// non-finite, `Err(PiggybackError::TangentDivergence)` when the primal
246/// stays finite but the tangent overflows (ratio-converging case), or
247/// `Err(PiggybackError::IterationsExhaustedTangent { iteration, z_norm })` when
248/// `max_iter` is reached before both deltas satisfy `tol`.
249pub fn piggyback_tangent_solve<F: Float>(
250    step_tape: &BytecodeTape<F>,
251    z0: &[F],
252    x: &[F],
253    x_dot: &[F],
254    num_states: usize,
255    max_iter: usize,
256    tol: F,
257) -> Result<(Vec<F>, Vec<F>, usize), PiggybackError> {
258    // Runtime vector-length check at solve-level: surfaces dimension
259    // mismatches as `Err` before the first iteration dispatches to the
260    // step fn (which still panics on bad input as a contract-level
261    // guarantee).
262    if x_dot.len() != x.len() {
263        return Err(PiggybackError::DimensionMismatch {
264            field: "x_dot",
265            expected: x.len(),
266            actual: x_dot.len(),
267        });
268    }
269    let m = num_states;
270    let mut z = z0.to_vec();
271    let mut z_dot = vec![F::zero(); m];
272    let mut buf = Vec::new();
273    let mut last_norm: f64 = f64::NAN;
274
275    for k in 0..max_iter {
276        let (z_new, z_dot_new) =
277            piggyback_tangent_step_with_buf(step_tape, &z, x, &z_dot, x_dot, num_states, &mut buf);
278
279        // Relative convergence: ||z_new - z|| / (1 + ||z||)
280        let mut delta_sq = F::zero();
281        let mut z_sq = F::zero();
282        for i in 0..m {
283            let d = z_new[i] - z[i];
284            delta_sq = delta_sq + d * d;
285            z_sq = z_sq + z[i] * z[i];
286        }
287        let norm = delta_sq.sqrt() / (F::one() + z_sq.sqrt());
288        // Variant-mapping order: norm-check first → PrimalDivergence;
289        // tangent-finite check second → TangentDivergence. A non-finite
290        // primal naturally produces a non-finite norm, so it falls into
291        // PrimalDivergence by detection priority.
292        if !norm.is_finite() {
293            return Err(PiggybackError::PrimalDivergence {
294                iteration: k,
295                last_norm: norm.to_f64().unwrap_or(f64::NAN),
296            });
297        }
298        // Detect tangent divergence even when the primal `z_new` itself is
299        // finite: the JVP iteration `z_dot_{k+1} = G_z·z_dot_k + G_x·x_dot`
300        // can produce Inf/NaN tangents that a primal-only norm check misses.
301        if !z_dot_new.iter().all(|v| v.is_finite()) {
302            // `norm` is guaranteed finite here — the `!norm.is_finite()`
303            // branch above would have returned `PrimalDivergence` first.
304            // The debug_assert guards against a future refactor that
305            // reorders these checks and silently invalidates the
306            // `TangentDivergence::last_norm` docstring's "finite by
307            // construction" promise.
308            debug_assert!(
309                norm.is_finite(),
310                "TangentDivergence path must see a finite primal norm"
311            );
312            return Err(PiggybackError::TangentDivergence {
313                iteration: k,
314                last_norm: norm.to_f64().unwrap_or(f64::NAN),
315            });
316        }
317        // Tangent convergence, mirroring the primal's relative-delta form
318        // (and `piggyback_forward_adjoint_solve`'s two-stream gate). The
319        // tangent starts at zero, so its delta shrinks on its own schedule;
320        // gating on the primal alone would return early with a truncated
321        // Neumann sum whenever the primal is warm-started. A non-finite
322        // `tangent_norm` cannot fake convergence (`NaN < tol` and
323        // `Inf < tol` are both false), and non-finite tangents were already
324        // rejected componentwise above.
325        let mut tangent_delta_sq = F::zero();
326        let mut tangent_sq = F::zero();
327        for i in 0..m {
328            let d = z_dot_new[i] - z_dot[i];
329            tangent_delta_sq = tangent_delta_sq + d * d;
330            tangent_sq = tangent_sq + z_dot[i] * z_dot[i];
331        }
332        let tangent_norm = tangent_delta_sq.sqrt() / (F::one() + tangent_sq.sqrt());
333
334        last_norm = norm.to_f64().unwrap_or(f64::NAN);
335        if norm < tol && tangent_norm < tol {
336            return Ok((z_new, z_dot_new, k + 1));
337        }
338
339        z = z_new;
340        z_dot = z_dot_new;
341    }
342
343    Err(PiggybackError::IterationsExhaustedTangent {
344        iteration: max_iter,
345        z_norm: last_norm,
346    })
347}
348
349/// Adjoint piggyback solve at a converged fixed point z* = G(z*, x).
350///
351/// Iterates the adjoint fixed-point equation `λ_{k+1} = G_z^T · λ_k + z̄`
352/// using reverse-mode sweeps through the step tape. At convergence, returns
353/// `x̄ = G_x^T · λ*`.
354///
355/// Requires z* to already be computed (e.g. by the primal solver).
356/// The iteration converges when G is a contraction (‖G_z‖ < 1).
357///
358/// Returns `Ok((x_bar, iterations))` on convergence. Returns
359/// `Err(PiggybackError::AdjointDivergence)` when the adjoint norm is
360/// non-finite or `lambda_new` overflows (ratio-converging case), or
361/// `Err(PiggybackError::IterationsExhaustedAdjoint { iteration, lam_norm })`
362/// when `max_iter` is reached without satisfying `tol`.
363pub fn piggyback_adjoint_solve<F: Float>(
364    step_tape: &mut BytecodeTape<F>,
365    z_star: &[F],
366    x: &[F],
367    z_bar: &[F],
368    num_states: usize,
369    max_iter: usize,
370    tol: F,
371) -> Result<(Vec<F>, usize), PiggybackError> {
372    // Runtime arg-length check first so solve-fn users see
373    // `Err(DimensionMismatch)` in preference to a `validate_step_tape`
374    // panic when both would fire. Matches the check ordering in
375    // `piggyback_tangent_solve`.
376    let m = num_states;
377    if z_bar.len() != m {
378        return Err(PiggybackError::DimensionMismatch {
379            field: "z_bar",
380            expected: m,
381            actual: z_bar.len(),
382        });
383    }
384    validate_step_tape(step_tape, z_star, x, num_states);
385
386    // Set primal values: forward([z*, x])
387    let mut input = Vec::with_capacity(m + x.len());
388    input.extend_from_slice(z_star);
389    input.extend_from_slice(x);
390    step_tape.forward(&input);
391
392    let mut lambda = z_bar.to_vec();
393    let mut last_norm: f64 = f64::NAN;
394
395    for k in 0..max_iter {
396        // reverse_seeded(λ) returns [G_z^T · λ; G_x^T · λ] (length m+n)
397        let adj = step_tape.reverse_seeded(&lambda);
398
399        // λ_new[i] = adj[i] + z_bar[i] for i = 0..m
400        let mut lambda_new = Vec::with_capacity(m);
401        let mut delta_sq = F::zero();
402        let mut lam_sq = F::zero();
403        for i in 0..m {
404            let l_new = adj[i] + z_bar[i];
405            let d = l_new - lambda[i];
406            delta_sq = delta_sq + d * d;
407            lam_sq = lam_sq + lambda[i] * lambda[i];
408            lambda_new.push(l_new);
409        }
410
411        let norm = delta_sq.sqrt() / (F::one() + lam_sq.sqrt());
412        if !norm.is_finite() {
413            return Err(PiggybackError::AdjointDivergence {
414                iteration: k,
415                last_norm: norm.to_f64().unwrap_or(f64::NAN),
416            });
417        }
418        // A ratio-converging iteration with exponentially-growing `lambda`
419        // magnitudes (spectral radius of `G_z^T` ≥ 1) can produce finite
420        // `norm` while `lambda_new` is Inf/NaN. Explicit finite check
421        // catches the divergence regardless of ratio behaviour.
422        if !lambda_new.iter().all(|v| v.is_finite()) {
423            debug_assert!(
424                norm.is_finite(),
425                "AdjointDivergence componentwise path must see a finite norm"
426            );
427            return Err(PiggybackError::AdjointDivergence {
428                iteration: k,
429                last_norm: norm.to_f64().unwrap_or(f64::NAN),
430            });
431        }
432        last_norm = norm.to_f64().unwrap_or(f64::NAN);
433        if norm < tol {
434            // One extra reverse pass with converged lambda to get consistent x_bar.
435            // Without this, adj[m..] uses the pre-convergence lambda, introducing
436            // O(tol * ||G_x||) error.
437            let adj_final = step_tape.reverse_seeded(&lambda_new);
438            return Ok((adj_final[m..].to_vec(), k + 1));
439        }
440
441        lambda = lambda_new;
442    }
443
444    Err(PiggybackError::IterationsExhaustedAdjoint {
445        iteration: max_iter,
446        lam_norm: last_norm,
447    })
448}
449
450/// Interleaved forward-adjoint piggyback solve.
451///
452/// Simultaneously iterates the primal fixed-point `z_{k+1} = G(z_k, x)` and
453/// the adjoint equation `λ_{k+1} = G_z^T · λ_k + z̄`. This cuts the total
454/// iteration count from `K_primal + K_adjoint` to `max(K_primal, K_adjoint)`.
455///
456/// Returns `Ok((z_star, x_bar, iterations))` when both `z` and `λ` converge.
457/// Returns `Err(PiggybackError::PrimalDivergence)` when `z_norm` becomes
458/// non-finite or `z_new` itself contains non-finite components,
459/// `Err(PiggybackError::AdjointDivergence)` when the adjoint norm or
460/// `lambda_new` overflows, or
461/// `Err(PiggybackError::IterationsExhaustedForwardAdjoint { iteration, z_norm, lam_norm })`
462/// when `max_iter` is reached without satisfying `tol`.
463pub fn piggyback_forward_adjoint_solve<F: Float>(
464    step_tape: &mut BytecodeTape<F>,
465    z0: &[F],
466    x: &[F],
467    z_bar: &[F],
468    num_states: usize,
469    max_iter: usize,
470    tol: F,
471) -> Result<(Vec<F>, Vec<F>, usize), PiggybackError> {
472    // Runtime arg-length check first — see note on
473    // `piggyback_adjoint_solve`.
474    let m = num_states;
475    if z_bar.len() != m {
476        return Err(PiggybackError::DimensionMismatch {
477            field: "z_bar",
478            expected: m,
479            actual: z_bar.len(),
480        });
481    }
482    validate_step_tape(step_tape, z0, x, num_states);
483
484    // Pre-allocate input buffer [z, x]
485    let mut input = Vec::with_capacity(m + x.len());
486    input.extend_from_slice(z0);
487    input.extend_from_slice(x);
488
489    let mut lambda = z_bar.to_vec();
490    let mut last_z_norm: f64 = f64::NAN;
491    let mut last_lam_norm: f64 = f64::NAN;
492
493    for k in 0..max_iter {
494        // Forward pass at current z
495        step_tape.forward(&input);
496        let z_new = step_tape.output_values();
497
498        // Reverse pass with current λ
499        let adj = step_tape.reverse_seeded(&lambda);
500
501        // Primal convergence: ||z_new - z|| / (1 + ||z||)
502        let mut z_delta_sq = F::zero();
503        let mut z_sq = F::zero();
504        for i in 0..m {
505            let d = z_new[i] - input[i];
506            z_delta_sq = z_delta_sq + d * d;
507            z_sq = z_sq + input[i] * input[i];
508        }
509        let z_norm = z_delta_sq.sqrt() / (F::one() + z_sq.sqrt());
510        if !z_norm.is_finite() {
511            return Err(PiggybackError::PrimalDivergence {
512                iteration: k,
513                last_norm: z_norm.to_f64().unwrap_or(f64::NAN),
514            });
515        }
516
517        // Adjoint update and convergence: λ_new = G_z^T · λ + z̄
518        let mut lam_delta_sq = F::zero();
519        let mut lam_sq = F::zero();
520        let mut lambda_new = Vec::with_capacity(m);
521        for i in 0..m {
522            let l_new = adj[i] + z_bar[i];
523            let d = l_new - lambda[i];
524            lam_delta_sq = lam_delta_sq + d * d;
525            lam_sq = lam_sq + lambda[i] * lambda[i];
526            lambda_new.push(l_new);
527        }
528        let lam_norm = lam_delta_sq.sqrt() / (F::one() + lam_sq.sqrt());
529        if !lam_norm.is_finite() {
530            return Err(PiggybackError::AdjointDivergence {
531                iteration: k,
532                last_norm: lam_norm.to_f64().unwrap_or(f64::NAN),
533            });
534        }
535        // Same divergence case as the standalone solvers: a ratio-converging
536        // iteration with exponentially-growing lambda magnitudes can produce
537        // finite `lam_norm` while `lambda_new` itself is Inf/NaN.
538        if !lambda_new.iter().all(|v| v.is_finite()) {
539            debug_assert!(
540                lam_norm.is_finite(),
541                "AdjointDivergence componentwise path must see a finite lam_norm"
542            );
543            return Err(PiggybackError::AdjointDivergence {
544                iteration: k,
545                last_norm: lam_norm.to_f64().unwrap_or(f64::NAN),
546            });
547        }
548        // Defense-in-depth: a non-finite `z_new[i]` would typically have
549        // already shown up as `!z_norm.is_finite()` above (the delta/sq
550        // loops touch every index), but guard the componentwise case
551        // explicitly so a future refactor of the norm computation can't
552        // silently lose primal-divergence detection.
553        if !z_new.iter().all(|v| v.is_finite()) {
554            debug_assert!(
555                z_norm.is_finite(),
556                "PrimalDivergence componentwise path must see a finite z_norm"
557            );
558            return Err(PiggybackError::PrimalDivergence {
559                iteration: k,
560                last_norm: z_norm.to_f64().unwrap_or(f64::NAN),
561            });
562        }
563
564        last_z_norm = z_norm.to_f64().unwrap_or(f64::NAN);
565        last_lam_norm = lam_norm.to_f64().unwrap_or(f64::NAN);
566
567        if z_norm < tol && lam_norm < tol {
568            // One extra reverse pass with converged lambda_new to get consistent x_bar,
569            // matching the pattern in piggyback_adjoint_solve.
570            input[..m].copy_from_slice(&z_new[..m]);
571            step_tape.forward(&input);
572            let adj_final = step_tape.reverse_seeded(&lambda_new);
573            return Ok((z_new, adj_final[m..].to_vec(), k + 1));
574        }
575
576        // Update z in the input buffer
577        input[..m].copy_from_slice(&z_new[..m]);
578        lambda = lambda_new;
579    }
580
581    Err(PiggybackError::IterationsExhaustedForwardAdjoint {
582        iteration: max_iter,
583        z_norm: last_z_norm,
584        lam_norm: last_lam_norm,
585    })
586}