Skip to main content

phasesmith_workflows/
tof_structural_multibank_solver.rs

1//! Bounded accepted-state solver for the guarded structural TOF objective.
2
3use std::error::Error;
4use std::fmt::{Display, Formatter};
5
6use nalgebra::{DMatrix, DVector};
7
8use crate::{
9    CancellationToken, DiagnosticValue, ParameterChange, ParameterError, ParameterSet,
10    PreparedStructuralTofMultiBankObjective, RefinementEventKind, RefinementLimits,
11    RefinementRuntime, RuntimeError, StructuralTofMultiBankCalculation,
12    StructuralTofMultiBankError, StructuralTofMultiBankInput, StructuralTofMultiBankLayout,
13    TerminationReason,
14};
15
16/// Numerical and bounded-runtime controls for structural multi-bank TOF.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct StructuralTofMultiBankRefinementOptions {
19    /// Hard iteration/evaluation/time/rejection limits.
20    pub limits: RefinementLimits,
21    /// Minimum accepted iterations before objective convergence.
22    pub min_iterations: usize,
23    /// Relative accepted objective-change tolerance.
24    pub objective_tolerance: f64,
25    /// Scaled step-norm tolerance.
26    pub parameter_tolerance: f64,
27    /// Initial positive Levenberg damping in scaled coordinates.
28    pub initial_damping: f64,
29    /// Multiplier applied after an unsuccessful iteration.
30    pub damping_increase: f64,
31    /// Multiplier applied after an accepted iteration.
32    pub damping_decrease: f64,
33    /// Maximum Euclidean scaled step norm.
34    pub max_scaled_parameter_step: f64,
35    /// Number of half-step retries after the full trial.
36    pub max_backtracks: usize,
37}
38
39impl StructuralTofMultiBankRefinementOptions {
40    /// Construct validated structural TOF numerical controls.
41    ///
42    /// # Errors
43    ///
44    /// Returns [`StructuralTofMultiBankRefinementError::InvalidOptions`] for
45    /// invalid tolerances, damping, iteration, or step controls.
46    #[allow(clippy::too_many_arguments)]
47    pub fn new(
48        limits: RefinementLimits,
49        min_iterations: usize,
50        objective_tolerance: f64,
51        parameter_tolerance: f64,
52        initial_damping: f64,
53        damping_increase: f64,
54        damping_decrease: f64,
55        max_scaled_parameter_step: f64,
56        max_backtracks: usize,
57    ) -> Result<Self, StructuralTofMultiBankRefinementError> {
58        let result = Self {
59            limits,
60            min_iterations,
61            objective_tolerance,
62            parameter_tolerance,
63            initial_damping,
64            damping_increase,
65            damping_decrease,
66            max_scaled_parameter_step,
67            max_backtracks,
68        };
69        result.validate()?;
70        Ok(result)
71    }
72
73    /// Revalidate adapter-decoded structural TOF controls.
74    ///
75    /// # Errors
76    ///
77    /// Returns [`StructuralTofMultiBankRefinementError::InvalidOptions`] for
78    /// invalid fields.
79    pub fn validate(self) -> Result<(), StructuralTofMultiBankRefinementError> {
80        let positive = [
81            self.objective_tolerance,
82            self.parameter_tolerance,
83            self.initial_damping,
84            self.damping_increase,
85            self.damping_decrease,
86            self.max_scaled_parameter_step,
87        ];
88        if self.min_iterations == 0
89            || self.min_iterations > self.limits.max_iterations()
90            || positive
91                .iter()
92                .any(|value| !value.is_finite() || *value <= 0.0)
93            || self.damping_increase <= 1.0
94            || self.damping_decrease >= 1.0
95        {
96            return Err(StructuralTofMultiBankRefinementError::InvalidOptions);
97        }
98        Ok(())
99    }
100}
101
102/// One atomically accepted structural TOF step.
103#[derive(Clone, Debug, PartialEq)]
104pub struct StructuralTofMultiBankIterationRecord {
105    /// One-based accepted iteration.
106    pub iteration: usize,
107    /// Accepted half summed weighted residual square.
108    pub objective: f64,
109    /// Previous objective minus accepted objective.
110    pub objective_change: f64,
111    /// Accepted scaled Euclidean step norm.
112    pub scaled_step_norm: f64,
113    /// Damping used for the accepted trial.
114    pub damping: f64,
115    /// Half-step backtracks used.
116    pub backtracks: usize,
117    /// Accepted physical changes in stable joint order.
118    pub parameter_changes: Vec<ParameterChange>,
119}
120
121/// Complete last-accepted state for deterministic continuation.
122#[derive(Clone, Debug, PartialEq)]
123pub struct StructuralTofMultiBankCheckpoint {
124    /// Exact original request contract.
125    pub request: StructuralTofMultiBankInput,
126    /// Last accepted physical request state.
127    pub input: StructuralTofMultiBankInput,
128    /// Last accepted joint physical parameters.
129    pub parameters: ParameterSet,
130    /// Last accepted objective.
131    pub objective: f64,
132    /// Damping for the next attempted iteration.
133    pub damping: f64,
134    /// Complete accepted history.
135    pub history: Vec<StructuralTofMultiBankIterationRecord>,
136}
137
138impl StructuralTofMultiBankCheckpoint {
139    /// Return the number of accepted iterations.
140    #[must_use]
141    pub fn completed_iterations(&self) -> usize {
142        self.history.len()
143    }
144
145    /// Revalidate this checkpoint against an exact request.
146    ///
147    /// # Errors
148    ///
149    /// Returns [`StructuralTofMultiBankRefinementError`] when any contract or
150    /// accepted numerical invariant is stale.
151    pub fn validate_for(
152        &self,
153        request: &StructuralTofMultiBankInput,
154    ) -> Result<(), StructuralTofMultiBankRefinementError> {
155        if &self.request != request
156            || !self.objective.is_finite()
157            || self.objective < 0.0
158            || !self.damping.is_finite()
159            || self.damping <= 0.0
160            || self.history.iter().enumerate().any(|(index, row)| {
161                row.iteration != index + 1
162                    || !row.objective.is_finite()
163                    || row.objective < 0.0
164                    || !row.objective_change.is_finite()
165                    || row.objective_change < 0.0
166                    || !row.scaled_step_norm.is_finite()
167                    || row.scaled_step_norm < 0.0
168                    || !row.damping.is_finite()
169                    || row.damping <= 0.0
170                    || row.parameter_changes.iter().any(|change| {
171                        !change.before.is_finite()
172                            || !change.after.is_finite()
173                            || !change.scaled_change.is_finite()
174                    })
175            })
176            || self
177                .history
178                .last()
179                .is_some_and(|row| row.objective.to_bits() != self.objective.to_bits())
180        {
181            return Err(StructuralTofMultiBankRefinementError::InvalidCheckpoint);
182        }
183        let request_layout = StructuralTofMultiBankLayout::new(request)?;
184        let accepted_layout = StructuralTofMultiBankLayout::new(&self.input)?;
185        if !same_contract(request_layout.parameters(), &self.parameters)
186            || !same_values(accepted_layout.parameters(), &self.parameters)
187        {
188            return Err(StructuralTofMultiBankRefinementError::InvalidCheckpoint);
189        }
190        Ok(())
191    }
192}
193
194/// Final accepted structural TOF refinement state.
195#[derive(Clone, Debug, PartialEq)]
196pub struct StructuralTofMultiBankRefinementResult {
197    /// Final accepted shared/local request state.
198    pub input: StructuralTofMultiBankInput,
199    /// Final display-ready bank calculations.
200    pub calculation: StructuralTofMultiBankCalculation,
201    /// Final physical parameters.
202    pub parameters: ParameterSet,
203    /// Complete accepted history.
204    pub history: Vec<StructuralTofMultiBankIterationRecord>,
205    /// Stable bounded termination category.
206    pub termination_reason: TerminationReason,
207    /// Restartable final checkpoint.
208    pub checkpoint: StructuralTofMultiBankCheckpoint,
209    /// Model evaluations consumed by this call.
210    pub evaluations: usize,
211}
212
213/// Refine a guarded structural TOF objective through a process-local runtime.
214///
215/// # Errors
216///
217/// Returns [`StructuralTofMultiBankRefinementError`] for invalid controls,
218/// checkpoint state, objective products, or linear solves.
219pub fn refine_structural_tof_multibank(
220    input: &StructuralTofMultiBankInput,
221    options: StructuralTofMultiBankRefinementOptions,
222    checkpoint: Option<&StructuralTofMultiBankCheckpoint>,
223    cancellation: Option<CancellationToken>,
224) -> Result<StructuralTofMultiBankRefinementResult, StructuralTofMultiBankRefinementError> {
225    let mut runtime = RefinementRuntime::new(options.limits, cancellation)?;
226    refine_structural_tof_multibank_with_runtime(input, options, checkpoint, &mut runtime)
227}
228
229/// Refine with caller-owned cancellation, event, and checkpoint sinks.
230///
231/// One trial installs all shared and bank-local values and is published only
232/// after the summed objective decreases.
233///
234/// # Errors
235///
236/// Returns [`StructuralTofMultiBankRefinementError`] for invalid state.
237#[allow(clippy::too_many_lines)]
238pub fn refine_structural_tof_multibank_with_runtime(
239    input: &StructuralTofMultiBankInput,
240    options: StructuralTofMultiBankRefinementOptions,
241    checkpoint: Option<&StructuralTofMultiBankCheckpoint>,
242    runtime: &mut RefinementRuntime<StructuralTofMultiBankCheckpoint>,
243) -> Result<StructuralTofMultiBankRefinementResult, StructuralTofMultiBankRefinementError> {
244    options
245        .validate()
246        .map_err(|_| StructuralTofMultiBankRefinementError::InvalidOptions)?;
247    let initial_layout = StructuralTofMultiBankLayout::new(input)?;
248    if initial_layout.parameters().specs().is_empty() {
249        return Err(StructuralTofMultiBankRefinementError::NoParameters);
250    }
251    let (mut live, mut parameters, mut history, mut damping) = if let Some(checkpoint) = checkpoint
252    {
253        checkpoint.validate_for(input)?;
254        runtime.resume_accepted(checkpoint.completed_iterations())?;
255        (
256            checkpoint.input.clone(),
257            checkpoint.parameters.clone(),
258            checkpoint.history.clone(),
259            checkpoint.damping,
260        )
261    } else {
262        (
263            input.clone(),
264            initial_layout.parameters().clone(),
265            Vec::new(),
266            options.initial_damping,
267        )
268    };
269    runtime.emit(
270        RefinementEventKind::Start,
271        "structural_tof_multibank",
272        "joint structural TOF refinement started",
273        vec![(
274            "parameter_count".to_owned(),
275            DiagnosticValue::Unsigned(parameters.specs().len() as u64),
276        )],
277    )?;
278    let mut termination = TerminationReason::MaxIterations;
279    'iterations: for iteration in history.len() + 1..=options.limits.max_iterations() {
280        if let Err(error) = runtime.begin_iteration(iteration) {
281            termination = normal_stop(error)?;
282            break;
283        }
284        if let Err(error) = runtime.begin_evaluation() {
285            termination = normal_stop(error)?;
286            break;
287        }
288        let objective = PreparedStructuralTofMultiBankObjective::new(live.clone())?;
289        let evaluated = objective.gradient()?;
290        let current_objective = evaluated.calculation.objective;
291        let scales = parameters
292            .specs()
293            .iter()
294            .map(crate::ParameterSpec::scale)
295            .collect::<Vec<_>>();
296        let rhs = evaluated
297            .gradient
298            .iter()
299            .zip(&scales)
300            .map(|(gradient, scale)| -gradient * scale)
301            .collect::<Vec<_>>();
302        let mut normal = match scaled_normal_matrix(&objective, &scales, runtime) {
303            Ok(normal) => normal,
304            Err(StructuralTofMultiBankRefinementError::Runtime(RuntimeError::Stopped(stop))) => {
305                termination = stop.reason;
306                break;
307            }
308            Err(error) => return Err(error),
309        };
310        for index in 0..normal.nrows() {
311            normal[(index, index)] += damping;
312        }
313        let rhs = DVector::from_vec(rhs);
314        let mut step = normal
315            .clone()
316            .lu()
317            .solve(&rhs)
318            .or_else(|| normal.svd(true, true).solve(&rhs, 1.0e-12).ok())
319            .ok_or(StructuralTofMultiBankRefinementError::LinearSolve)?;
320        if step.iter().any(|value| !value.is_finite()) {
321            return Err(StructuralTofMultiBankRefinementError::LinearSolve);
322        }
323        let mut step_norm = step.norm();
324        if step_norm > options.max_scaled_parameter_step {
325            step *= options.max_scaled_parameter_step / step_norm;
326            step_norm = options.max_scaled_parameter_step;
327        }
328        if step_norm < options.parameter_tolerance {
329            termination = TerminationReason::Converged;
330            break;
331        }
332        let current = parameters
333            .specs()
334            .iter()
335            .map(crate::ParameterSpec::value)
336            .collect::<Vec<_>>();
337        let mut accepted = None;
338        for backtrack in 0..=options.max_backtracks {
339            let factor = 0.5_f64.powi(i32::try_from(backtrack).unwrap_or(i32::MAX));
340            let trial_values = parameters
341                .specs()
342                .iter()
343                .zip(step.iter())
344                .map(|(spec, delta)| {
345                    spec.bounds()
346                        .clip(spec.value() + factor * spec.scale() * delta)
347                })
348                .collect::<Vec<_>>();
349            let Ok(trial) = objective
350                .layout()
351                .apply_value_change(&live, &current, &trial_values)
352            else {
353                runtime.reject_step().map_err(normal_or_error)?;
354                continue;
355            };
356            if let Err(error) = runtime.begin_evaluation() {
357                termination = normal_stop(error)?;
358                break 'iterations;
359            }
360            let calculation =
361                PreparedStructuralTofMultiBankObjective::new(trial.clone())?.calculate()?;
362            if calculation.objective < current_objective {
363                accepted = Some((backtrack, factor, trial_values, trial, calculation));
364                break;
365            }
366            if let Err(error) = runtime.reject_step() {
367                termination = normal_stop(error)?;
368                break 'iterations;
369            }
370        }
371        let Some((backtracks, factor, trial_values, trial, calculation)) = accepted else {
372            damping *= options.damping_increase;
373            continue;
374        };
375        let objective_change = current_objective - calculation.objective;
376        let parameter_changes = parameters
377            .specs()
378            .iter()
379            .zip(&current)
380            .zip(&trial_values)
381            .filter(|((_, before), after)| before.to_bits() != after.to_bits())
382            .map(|((spec, before), after)| ParameterChange {
383                key: spec.key().clone(),
384                before: *before,
385                after: *after,
386                scaled_change: (after - before) / spec.scale(),
387            })
388            .collect();
389        history.push(StructuralTofMultiBankIterationRecord {
390            iteration: history.len() + 1,
391            objective: calculation.objective,
392            objective_change,
393            scaled_step_norm: factor * step_norm,
394            damping,
395            backtracks,
396            parameter_changes,
397        });
398        let accepted_layout = StructuralTofMultiBankLayout::new(&trial)?;
399        let accepted_values = accepted_layout
400            .parameters()
401            .specs()
402            .iter()
403            .map(crate::ParameterSpec::value)
404            .collect::<Vec<_>>();
405        parameters = parameter_set_with_values(initial_layout.parameters(), &accepted_values)?;
406        live = trial;
407        damping = (damping * options.damping_decrease).max(1.0e-18);
408        let state = checkpoint_state(
409            input,
410            &live,
411            &parameters,
412            calculation.objective,
413            damping,
414            &history,
415        );
416        runtime.accept_step(Some(&state))?;
417        runtime.emit(
418            RefinementEventKind::StepAccepted,
419            "structural_tof_multibank_step",
420            "joint structural TOF step accepted",
421            vec![(
422                "objective".to_owned(),
423                DiagnosticValue::Float(calculation.objective),
424            )],
425        )?;
426        if history.len() >= options.min_iterations
427            && objective_change <= options.objective_tolerance * calculation.objective.max(1.0)
428        {
429            termination = TerminationReason::Converged;
430            break;
431        }
432    }
433    runtime.begin_evaluation().or_else(|error| match error {
434        RuntimeError::Stopped(_) => Ok(()),
435        other => Err(other),
436    })?;
437    let calculation = PreparedStructuralTofMultiBankObjective::new(live.clone())?.calculate()?;
438    let checkpoint = checkpoint_state(
439        input,
440        &live,
441        &parameters,
442        calculation.objective,
443        damping,
444        &history,
445    );
446    checkpoint.validate_for(input)?;
447    runtime.emit(
448        RefinementEventKind::Termination,
449        "structural_tof_multibank",
450        "joint structural TOF refinement terminated",
451        vec![(
452            "reason".to_owned(),
453            DiagnosticValue::String(termination.as_str().to_owned()),
454        )],
455    )?;
456    Ok(StructuralTofMultiBankRefinementResult {
457        input: live,
458        calculation,
459        parameters,
460        history,
461        termination_reason: termination,
462        checkpoint,
463        evaluations: runtime.evaluations(),
464    })
465}
466
467fn scaled_normal_matrix(
468    objective: &PreparedStructuralTofMultiBankObjective,
469    scales: &[f64],
470    runtime: &mut RefinementRuntime<StructuralTofMultiBankCheckpoint>,
471) -> Result<DMatrix<f64>, StructuralTofMultiBankRefinementError> {
472    let count = scales.len();
473    let mut matrix = DMatrix::zeros(count, count);
474    for column in 0..count {
475        runtime.begin_evaluation().map_err(normal_or_error)?;
476        let mut direction = vec![0.0; count];
477        direction[column] = scales[column];
478        let product = objective.normal_product(&direction, 0.0)?;
479        for row in 0..count {
480            matrix[(row, column)] = scales[row] * product[row];
481        }
482    }
483    Ok(0.5 * (&matrix + matrix.transpose()))
484}
485
486fn parameter_set_with_values(
487    template: &ParameterSet,
488    values: &[f64],
489) -> Result<ParameterSet, StructuralTofMultiBankRefinementError> {
490    let replacements = template
491        .specs()
492        .iter()
493        .zip(values)
494        .map(|(spec, value)| (spec.key().clone(), *value))
495        .collect();
496    Ok(template.replace_values(&replacements)?)
497}
498
499fn checkpoint_state(
500    request: &StructuralTofMultiBankInput,
501    input: &StructuralTofMultiBankInput,
502    parameters: &ParameterSet,
503    objective: f64,
504    damping: f64,
505    history: &[StructuralTofMultiBankIterationRecord],
506) -> StructuralTofMultiBankCheckpoint {
507    StructuralTofMultiBankCheckpoint {
508        request: request.clone(),
509        input: input.clone(),
510        parameters: parameters.clone(),
511        objective,
512        damping,
513        history: history.to_vec(),
514    }
515}
516
517fn same_contract(left: &ParameterSet, right: &ParameterSet) -> bool {
518    left.specs().len() == right.specs().len()
519        && left.specs().iter().zip(right.specs()).all(|(left, right)| {
520            left.key() == right.key()
521                && left.unit() == right.unit()
522                && left.bounds() == right.bounds()
523                && left.scale().to_bits() == right.scale().to_bits()
524        })
525}
526
527fn same_values(left: &ParameterSet, right: &ParameterSet) -> bool {
528    left.specs().len() == right.specs().len()
529        && left.specs().iter().zip(right.specs()).all(|(left, right)| {
530            left.key() == right.key() && left.value().to_bits() == right.value().to_bits()
531        })
532}
533
534fn normal_stop(
535    error: RuntimeError,
536) -> Result<TerminationReason, StructuralTofMultiBankRefinementError> {
537    match error {
538        RuntimeError::Stopped(stop) => Ok(stop.reason),
539        other => Err(StructuralTofMultiBankRefinementError::Runtime(other)),
540    }
541}
542
543fn normal_or_error(error: RuntimeError) -> StructuralTofMultiBankRefinementError {
544    StructuralTofMultiBankRefinementError::Runtime(error)
545}
546
547/// Invalid bounded structural TOF refinement state.
548#[derive(Debug)]
549pub enum StructuralTofMultiBankRefinementError {
550    /// Solver controls are invalid.
551    InvalidOptions,
552    /// No physical parameter was selected.
553    NoParameters,
554    /// Restart state is stale or numerically invalid.
555    InvalidCheckpoint,
556    /// The scaled normal system could not be solved.
557    LinearSolve,
558    /// Joint objective state is invalid.
559    Objective(StructuralTofMultiBankError),
560    /// Stable parameter replacement failed.
561    Parameter(ParameterError),
562    /// Runtime boundary failed.
563    Runtime(RuntimeError),
564}
565
566impl Display for StructuralTofMultiBankRefinementError {
567    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
568        match self {
569            Self::InvalidOptions => {
570                formatter.write_str("structural TOF solver options are invalid")
571            }
572            Self::NoParameters => formatter
573                .write_str("structural TOF solver requires at least one selected parameter"),
574            Self::InvalidCheckpoint => formatter.write_str("structural TOF checkpoint is invalid"),
575            Self::LinearSolve => {
576                formatter.write_str("structural TOF normal system could not be solved")
577            }
578            Self::Objective(error) => Display::fmt(error, formatter),
579            Self::Parameter(error) => Display::fmt(error, formatter),
580            Self::Runtime(error) => Display::fmt(error, formatter),
581        }
582    }
583}
584
585impl Error for StructuralTofMultiBankRefinementError {}
586
587impl From<StructuralTofMultiBankError> for StructuralTofMultiBankRefinementError {
588    fn from(value: StructuralTofMultiBankError) -> Self {
589        Self::Objective(value)
590    }
591}
592impl From<ParameterError> for StructuralTofMultiBankRefinementError {
593    fn from(value: ParameterError) -> Self {
594        Self::Parameter(value)
595    }
596}
597impl From<RuntimeError> for StructuralTofMultiBankRefinementError {
598    fn from(value: RuntimeError) -> Self {
599        Self::Runtime(value)
600    }
601}