1use 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#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct StructuralTofMultiBankRefinementOptions {
19 pub limits: RefinementLimits,
21 pub min_iterations: usize,
23 pub objective_tolerance: f64,
25 pub parameter_tolerance: f64,
27 pub initial_damping: f64,
29 pub damping_increase: f64,
31 pub damping_decrease: f64,
33 pub max_scaled_parameter_step: f64,
35 pub max_backtracks: usize,
37}
38
39impl StructuralTofMultiBankRefinementOptions {
40 #[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 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#[derive(Clone, Debug, PartialEq)]
104pub struct StructuralTofMultiBankIterationRecord {
105 pub iteration: usize,
107 pub objective: f64,
109 pub objective_change: f64,
111 pub scaled_step_norm: f64,
113 pub damping: f64,
115 pub backtracks: usize,
117 pub parameter_changes: Vec<ParameterChange>,
119}
120
121#[derive(Clone, Debug, PartialEq)]
123pub struct StructuralTofMultiBankCheckpoint {
124 pub request: StructuralTofMultiBankInput,
126 pub input: StructuralTofMultiBankInput,
128 pub parameters: ParameterSet,
130 pub objective: f64,
132 pub damping: f64,
134 pub history: Vec<StructuralTofMultiBankIterationRecord>,
136}
137
138impl StructuralTofMultiBankCheckpoint {
139 #[must_use]
141 pub fn completed_iterations(&self) -> usize {
142 self.history.len()
143 }
144
145 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#[derive(Clone, Debug, PartialEq)]
196pub struct StructuralTofMultiBankRefinementResult {
197 pub input: StructuralTofMultiBankInput,
199 pub calculation: StructuralTofMultiBankCalculation,
201 pub parameters: ParameterSet,
203 pub history: Vec<StructuralTofMultiBankIterationRecord>,
205 pub termination_reason: TerminationReason,
207 pub checkpoint: StructuralTofMultiBankCheckpoint,
209 pub evaluations: usize,
211}
212
213pub 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#[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, ¤t, &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(¤t)
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 ¶meters,
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 ¶meters,
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#[derive(Debug)]
549pub enum StructuralTofMultiBankRefinementError {
550 InvalidOptions,
552 NoParameters,
554 InvalidCheckpoint,
556 LinearSolve,
558 Objective(StructuralTofMultiBankError),
560 Parameter(ParameterError),
562 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}