chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
@@ -177,6 +177,62 @@ impl JacobianMatrix {
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates the condition number of the Jacobian matrix.
|
||||
///
|
||||
/// The condition number κ = σ_max / σ_min indicates how ill-conditioned
|
||||
/// the matrix is. Values > 1e10 indicate an ill-conditioned system that
|
||||
/// may cause numerical instability in the solver.
|
||||
///
|
||||
/// Uses SVD decomposition to compute singular values. This is an O(n³)
|
||||
/// operation and should only be used for diagnostics.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Some(κ)` - The condition number (ratio of largest to smallest singular value)
|
||||
/// * `None` - If the matrix is rank-deficient (σ_min = 0)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use entropyk_solver::jacobian::JacobianMatrix;
|
||||
///
|
||||
/// // Well-conditioned matrix
|
||||
/// let entries = vec![(0, 0, 2.0), (1, 1, 1.0)];
|
||||
/// let j = JacobianMatrix::from_builder(&entries, 2, 2);
|
||||
/// let cond = j.estimate_condition_number().unwrap();
|
||||
/// assert!(cond < 10.0, "Expected low condition number, got {}", cond);
|
||||
///
|
||||
/// // Ill-conditioned matrix (nearly singular)
|
||||
/// let bad_entries = vec![(0, 0, 1.0), (0, 1, 1.0), (1, 0, 1.0), (1, 1, 1.0000001)];
|
||||
/// let bad_j = JacobianMatrix::from_builder(&bad_entries, 2, 2);
|
||||
/// let bad_cond = bad_j.estimate_condition_number().unwrap();
|
||||
/// assert!(bad_cond > 1e7, "Expected high condition number, got {}", bad_cond);
|
||||
/// ```
|
||||
pub fn estimate_condition_number(&self) -> Option<f64> {
|
||||
// Handle empty matrices
|
||||
if self.0.nrows() == 0 || self.0.ncols() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Use SVD to get singular values
|
||||
let svd = self.0.clone().svd(true, true);
|
||||
|
||||
// Get singular values
|
||||
let singular_values = svd.singular_values;
|
||||
|
||||
if singular_values.len() == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sigma_max = singular_values.max();
|
||||
let sigma_min = singular_values.iter().filter(|&&s| s > 0.0).min_by(|a, b| a.partial_cmp(b).unwrap()).copied();
|
||||
|
||||
match sigma_min {
|
||||
Some(min) => Some(sigma_max / min),
|
||||
None => None, // Matrix is rank-deficient
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a numerical Jacobian via finite differences.
|
||||
///
|
||||
/// For each state variable x_j, perturbs by epsilon and computes:
|
||||
|
||||
@@ -34,7 +34,9 @@ pub use jacobian::JacobianMatrix;
|
||||
pub use macro_component::{MacroComponent, MacroComponentSnapshot, PortMapping};
|
||||
pub use metadata::SimulationMetadata;
|
||||
pub use solver::{
|
||||
ConvergedState, ConvergenceStatus, JacobianFreezingConfig, Solver, SolverError, TimeoutConfig,
|
||||
ConvergedState, ConvergenceStatus, ConvergenceDiagnostics, IterationDiagnostics,
|
||||
JacobianFreezingConfig, Solver, SolverError, SolverSwitchEvent, SolverType, SwitchReason,
|
||||
TimeoutConfig, VerboseConfig, VerboseOutputFormat,
|
||||
};
|
||||
pub use strategies::{
|
||||
FallbackConfig, FallbackSolver, NewtonConfig, PicardConfig, SolverStrategy,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! Provides the `Solver` trait (object-safe interface) and `SolverStrategy` enum
|
||||
//! (zero-cost static dispatch) for solver strategies.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -126,6 +127,12 @@ pub struct ConvergedState {
|
||||
|
||||
/// Traceability metadata for reproducibility.
|
||||
pub metadata: SimulationMetadata,
|
||||
|
||||
/// Optional convergence diagnostics (Story 7.4).
|
||||
///
|
||||
/// `Some(diagnostics)` when verbose mode was enabled during solving.
|
||||
/// `None` when verbose mode was disabled (backward-compatible default).
|
||||
pub diagnostics: Option<ConvergenceDiagnostics>,
|
||||
}
|
||||
|
||||
impl ConvergedState {
|
||||
@@ -144,6 +151,7 @@ impl ConvergedState {
|
||||
status,
|
||||
convergence_report: None,
|
||||
metadata,
|
||||
diagnostics: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +171,27 @@ impl ConvergedState {
|
||||
status,
|
||||
convergence_report: Some(report),
|
||||
metadata,
|
||||
diagnostics: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `ConvergedState` with attached diagnostics.
|
||||
pub fn with_diagnostics(
|
||||
state: Vec<f64>,
|
||||
iterations: usize,
|
||||
final_residual: f64,
|
||||
status: ConvergenceStatus,
|
||||
metadata: SimulationMetadata,
|
||||
diagnostics: ConvergenceDiagnostics,
|
||||
) -> Self {
|
||||
Self {
|
||||
state,
|
||||
iterations,
|
||||
final_residual,
|
||||
status,
|
||||
convergence_report: None,
|
||||
metadata,
|
||||
diagnostics: Some(diagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +380,336 @@ impl Default for JacobianFreezingConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Verbose Mode Configuration (Story 7.4)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Output format for verbose diagnostics.
|
||||
///
|
||||
/// Controls how convergence diagnostics are presented to the user.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub enum VerboseOutputFormat {
|
||||
/// Output diagnostics via `tracing` logs only.
|
||||
Log,
|
||||
/// Output diagnostics as structured JSON.
|
||||
Json,
|
||||
/// Output via both logging and JSON.
|
||||
#[default]
|
||||
Both,
|
||||
}
|
||||
|
||||
/// Configuration for debug verbose mode in solvers.
|
||||
///
|
||||
/// When enabled, provides detailed convergence diagnostics to help debug
|
||||
/// non-converging thermodynamic systems. This includes per-iteration residuals,
|
||||
/// Jacobian condition numbers, solver switch events, and final state dumps.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use entropyk_solver::solver::{VerboseConfig, VerboseOutputFormat};
|
||||
///
|
||||
/// // Enable all verbose features
|
||||
/// let verbose = VerboseConfig {
|
||||
/// enabled: true,
|
||||
/// log_residuals: true,
|
||||
/// log_jacobian_condition: true,
|
||||
/// log_solver_switches: true,
|
||||
/// dump_final_state: true,
|
||||
/// output_format: VerboseOutputFormat::Both,
|
||||
/// };
|
||||
///
|
||||
/// // Default: all features disabled (backward compatible)
|
||||
/// let default_config = VerboseConfig::default();
|
||||
/// assert!(!default_config.enabled);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct VerboseConfig {
|
||||
/// Master switch for verbose mode.
|
||||
///
|
||||
/// When `false`, all verbose output is disabled regardless of other settings.
|
||||
/// Default: `false` (backward compatible).
|
||||
pub enabled: bool,
|
||||
|
||||
/// Log residuals at each iteration.
|
||||
///
|
||||
/// When `true`, emits `tracing::info!` logs with iteration number,
|
||||
/// residual norm, and delta from previous iteration.
|
||||
/// Default: `false`.
|
||||
pub log_residuals: bool,
|
||||
|
||||
/// Report Jacobian condition number.
|
||||
///
|
||||
/// When `true`, computes and logs the Jacobian condition number
|
||||
/// (ratio of largest to smallest singular values). Values > 1e10
|
||||
/// indicate an ill-conditioned system.
|
||||
/// Default: `false`.
|
||||
///
|
||||
/// **Note:** Condition number estimation is O(n³) and may impact
|
||||
/// performance for large systems.
|
||||
pub log_jacobian_condition: bool,
|
||||
|
||||
/// Log solver switch events.
|
||||
///
|
||||
/// When `true`, logs when the fallback solver switches between
|
||||
/// Newton-Raphson and Sequential Substitution, including the reason.
|
||||
/// Default: `false`.
|
||||
pub log_solver_switches: bool,
|
||||
|
||||
/// Dump final state on non-convergence.
|
||||
///
|
||||
/// When `true`, dumps the final state vector and diagnostics
|
||||
/// when the solver fails to converge, for post-mortem analysis.
|
||||
/// Default: `false`.
|
||||
pub dump_final_state: bool,
|
||||
|
||||
/// Output format for diagnostics.
|
||||
///
|
||||
/// Default: `VerboseOutputFormat::Both`.
|
||||
pub output_format: VerboseOutputFormat,
|
||||
}
|
||||
|
||||
impl Default for VerboseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
log_residuals: false,
|
||||
log_jacobian_condition: false,
|
||||
log_solver_switches: false,
|
||||
dump_final_state: false,
|
||||
output_format: VerboseOutputFormat::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VerboseConfig {
|
||||
/// Creates a new `VerboseConfig` with all features enabled.
|
||||
pub fn all_enabled() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
log_residuals: true,
|
||||
log_jacobian_condition: true,
|
||||
log_solver_switches: true,
|
||||
dump_final_state: true,
|
||||
output_format: VerboseOutputFormat::Both,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if any verbose feature is enabled.
|
||||
pub fn is_any_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
&& (self.log_residuals
|
||||
|| self.log_jacobian_condition
|
||||
|| self.log_solver_switches
|
||||
|| self.dump_final_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-iteration diagnostics captured during solving.
|
||||
///
|
||||
/// Records the state of the solver at each iteration for debugging
|
||||
/// and post-mortem analysis.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IterationDiagnostics {
|
||||
/// Iteration number (0-indexed).
|
||||
pub iteration: usize,
|
||||
|
||||
/// $\ell_2$ norm of the residual vector.
|
||||
pub residual_norm: f64,
|
||||
|
||||
/// Norm of the change from previous iteration ($\|\Delta x\|$).
|
||||
pub delta_norm: f64,
|
||||
|
||||
/// Line search step size (Newton-Raphson only).
|
||||
///
|
||||
/// `None` for Sequential Substitution or if line search was not used.
|
||||
pub alpha: Option<f64>,
|
||||
|
||||
/// Whether the Jacobian was reused (frozen) this iteration.
|
||||
pub jacobian_frozen: bool,
|
||||
|
||||
/// Jacobian condition number (if computed).
|
||||
///
|
||||
/// Only populated when `log_jacobian_condition` is enabled.
|
||||
pub jacobian_condition: Option<f64>,
|
||||
}
|
||||
|
||||
/// Type of solver being used.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SolverType {
|
||||
/// Newton-Raphson solver.
|
||||
NewtonRaphson,
|
||||
/// Sequential Substitution (Picard) solver.
|
||||
SequentialSubstitution,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SolverType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SolverType::NewtonRaphson => write!(f, "Newton-Raphson"),
|
||||
SolverType::SequentialSubstitution => write!(f, "Sequential Substitution"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reason for solver switch in fallback strategy.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SwitchReason {
|
||||
/// Newton-Raphson diverged (residual increasing).
|
||||
Divergence,
|
||||
/// Newton-Raphson converging too slowly.
|
||||
SlowConvergence,
|
||||
/// User explicitly requested switch.
|
||||
UserRequested,
|
||||
/// Returning to Newton-Raphson after Picard stabilized.
|
||||
ReturnToNewton,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SwitchReason {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SwitchReason::Divergence => write!(f, "divergence detected"),
|
||||
SwitchReason::SlowConvergence => write!(f, "slow convergence"),
|
||||
SwitchReason::UserRequested => write!(f, "user requested"),
|
||||
SwitchReason::ReturnToNewton => write!(f, "returning to Newton after stabilization"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Event record for solver switches in fallback strategy.
|
||||
///
|
||||
/// Captures when and why the solver switched between strategies.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SolverSwitchEvent {
|
||||
/// Solver being switched from.
|
||||
pub from_solver: SolverType,
|
||||
|
||||
/// Solver being switched to.
|
||||
pub to_solver: SolverType,
|
||||
|
||||
/// Reason for the switch.
|
||||
pub reason: SwitchReason,
|
||||
|
||||
/// Iteration number at which the switch occurred.
|
||||
pub iteration: usize,
|
||||
|
||||
/// Residual norm at the time of switch.
|
||||
pub residual_at_switch: f64,
|
||||
}
|
||||
|
||||
/// Comprehensive convergence diagnostics for a solve attempt.
|
||||
///
|
||||
/// Contains all diagnostic information collected during solving,
|
||||
/// suitable for JSON serialization and post-mortem analysis.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ConvergenceDiagnostics {
|
||||
/// Total iterations performed.
|
||||
pub iterations: usize,
|
||||
|
||||
/// Final residual norm.
|
||||
pub final_residual: f64,
|
||||
|
||||
/// Best residual norm achieved during iteration.
|
||||
pub best_residual: f64,
|
||||
|
||||
/// Whether the solver converged.
|
||||
pub converged: bool,
|
||||
|
||||
/// Per-iteration diagnostics history.
|
||||
pub iteration_history: Vec<IterationDiagnostics>,
|
||||
|
||||
/// Solver switch events (fallback strategy only).
|
||||
pub solver_switches: Vec<SolverSwitchEvent>,
|
||||
|
||||
/// Final state vector (populated on non-convergence if `dump_final_state` enabled).
|
||||
pub final_state: Option<Vec<f64>>,
|
||||
|
||||
/// Jacobian condition number at final iteration.
|
||||
pub jacobian_condition_final: Option<f64>,
|
||||
|
||||
/// Total solve time in milliseconds.
|
||||
pub timing_ms: u64,
|
||||
|
||||
/// Solver type used for the final iteration.
|
||||
pub final_solver: Option<SolverType>,
|
||||
}
|
||||
|
||||
impl ConvergenceDiagnostics {
|
||||
/// Creates a new empty `ConvergenceDiagnostics`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Pre-allocates iteration history for `max_iterations` entries.
|
||||
pub fn with_capacity(max_iterations: usize) -> Self {
|
||||
Self {
|
||||
iteration_history: Vec::with_capacity(max_iterations),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an iteration's diagnostics to the history.
|
||||
pub fn push_iteration(&mut self, diagnostics: IterationDiagnostics) {
|
||||
self.iteration_history.push(diagnostics);
|
||||
}
|
||||
|
||||
/// Records a solver switch event.
|
||||
pub fn push_switch(&mut self, event: SolverSwitchEvent) {
|
||||
self.solver_switches.push(event);
|
||||
}
|
||||
|
||||
/// Returns a human-readable summary of the diagnostics.
|
||||
pub fn summary(&self) -> String {
|
||||
let converged_str = if self.converged { "YES" } else { "NO" };
|
||||
let switch_count = self.solver_switches.len();
|
||||
|
||||
let mut summary = format!(
|
||||
"Convergence Diagnostics Summary\n\
|
||||
===============================\n\
|
||||
Converged: {}\n\
|
||||
Iterations: {}\n\
|
||||
Final Residual: {:.3e}\n\
|
||||
Best Residual: {:.3e}\n\
|
||||
Solver Switches: {}\n\
|
||||
Timing: {} ms",
|
||||
converged_str,
|
||||
self.iterations,
|
||||
self.final_residual,
|
||||
self.best_residual,
|
||||
switch_count,
|
||||
self.timing_ms
|
||||
);
|
||||
|
||||
if let Some(cond) = self.jacobian_condition_final {
|
||||
summary.push_str(&format!("\nJacobian Condition: {:.3e}", cond));
|
||||
if cond > 1e10 {
|
||||
summary.push_str(" (WARNING: ill-conditioned)");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref solver) = self.final_solver {
|
||||
summary.push_str(&format!("\nFinal Solver: {}", solver));
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
/// Dumps diagnostics to the configured output format.
|
||||
///
|
||||
/// Returns JSON string if `format` is `Json` or `Both`, suitable for
|
||||
/// file output or structured logging.
|
||||
pub fn dump_diagnostics(&self, format: VerboseOutputFormat) -> String {
|
||||
match format {
|
||||
VerboseOutputFormat::Log => self.summary(),
|
||||
VerboseOutputFormat::Json | VerboseOutputFormat::Both => {
|
||||
serde_json::to_string_pretty(self).unwrap_or_else(|e| {
|
||||
format!("{{\"error\": \"Failed to serialize diagnostics: {}\"}}", e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper functions
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -25,7 +25,10 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use crate::criteria::ConvergenceCriteria;
|
||||
use crate::metadata::SimulationMetadata;
|
||||
use crate::solver::{ConvergedState, ConvergenceStatus, Solver, SolverError};
|
||||
use crate::solver::{
|
||||
ConvergedState, ConvergenceDiagnostics, ConvergenceStatus, Solver, SolverError,
|
||||
SolverSwitchEvent, SolverType, SwitchReason, VerboseConfig,
|
||||
};
|
||||
use crate::system::System;
|
||||
|
||||
use super::{NewtonConfig, PicardConfig};
|
||||
@@ -39,13 +42,14 @@ use super::{NewtonConfig, PicardConfig};
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use entropyk_solver::solver::{FallbackConfig, FallbackSolver, Solver};
|
||||
/// use entropyk_solver::solver::{FallbackConfig, FallbackSolver, Solver, VerboseConfig};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let config = FallbackConfig {
|
||||
/// fallback_enabled: true,
|
||||
/// return_to_newton_threshold: 1e-3,
|
||||
/// max_fallback_switches: 2,
|
||||
/// verbose_config: VerboseConfig::default(),
|
||||
/// };
|
||||
///
|
||||
/// let solver = FallbackSolver::new(config)
|
||||
@@ -71,6 +75,9 @@ pub struct FallbackConfig {
|
||||
/// Prevents infinite oscillation between Newton and Picard.
|
||||
/// Default: 2.
|
||||
pub max_fallback_switches: usize,
|
||||
|
||||
/// Verbose mode configuration for diagnostics.
|
||||
pub verbose_config: VerboseConfig,
|
||||
}
|
||||
|
||||
impl Default for FallbackConfig {
|
||||
@@ -79,6 +86,7 @@ impl Default for FallbackConfig {
|
||||
fallback_enabled: true,
|
||||
return_to_newton_threshold: 1e-3,
|
||||
max_fallback_switches: 2,
|
||||
verbose_config: VerboseConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +98,15 @@ enum CurrentSolver {
|
||||
Picard,
|
||||
}
|
||||
|
||||
impl From<CurrentSolver> for SolverType {
|
||||
fn from(solver: CurrentSolver) -> Self {
|
||||
match solver {
|
||||
CurrentSolver::Newton => SolverType::NewtonRaphson,
|
||||
CurrentSolver::Picard => SolverType::SequentialSubstitution,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal state for the fallback solver.
|
||||
struct FallbackState {
|
||||
current_solver: CurrentSolver,
|
||||
@@ -100,6 +117,10 @@ struct FallbackState {
|
||||
best_state: Option<Vec<f64>>,
|
||||
/// Best residual norm across all solver invocations (Story 4.5 - AC: #4)
|
||||
best_residual: Option<f64>,
|
||||
/// Total iterations across all solver invocations
|
||||
total_iterations: usize,
|
||||
/// Solver switch events for diagnostics (Story 7.4)
|
||||
switch_events: Vec<SolverSwitchEvent>,
|
||||
}
|
||||
|
||||
impl FallbackState {
|
||||
@@ -110,6 +131,8 @@ impl FallbackState {
|
||||
committed_to_picard: false,
|
||||
best_state: None,
|
||||
best_residual: None,
|
||||
total_iterations: 0,
|
||||
switch_events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +143,23 @@ impl FallbackState {
|
||||
self.best_residual = Some(residual);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a solver switch event (Story 7.4)
|
||||
fn record_switch(
|
||||
&mut self,
|
||||
from: CurrentSolver,
|
||||
to: CurrentSolver,
|
||||
reason: SwitchReason,
|
||||
residual_at_switch: f64,
|
||||
) {
|
||||
self.switch_events.push(SolverSwitchEvent {
|
||||
from_solver: from.into(),
|
||||
to_solver: to.into(),
|
||||
reason,
|
||||
iteration: self.total_iterations,
|
||||
residual_at_switch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Intelligent fallback solver that switches between Newton-Raphson and Picard.
|
||||
@@ -211,10 +251,23 @@ impl FallbackSolver {
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<ConvergedState, SolverError> {
|
||||
let mut state = FallbackState::new();
|
||||
|
||||
// Verbose mode setup
|
||||
let verbose_enabled = self.config.verbose_config.enabled
|
||||
&& self.config.verbose_config.is_any_enabled();
|
||||
let mut diagnostics = if verbose_enabled {
|
||||
Some(ConvergenceDiagnostics::with_capacity(100))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Pre-configure solver configs once
|
||||
let mut newton_cfg = self.newton_config.clone();
|
||||
let mut picard_cfg = self.picard_config.clone();
|
||||
|
||||
// Propagate verbose config to child solvers
|
||||
newton_cfg.verbose_config = self.config.verbose_config.clone();
|
||||
picard_cfg.verbose_config = self.config.verbose_config.clone();
|
||||
|
||||
loop {
|
||||
// Check remaining time budget
|
||||
@@ -242,6 +295,27 @@ impl FallbackSolver {
|
||||
Ok(converged) => {
|
||||
// Update best state tracking (Story 4.5 - AC: #4)
|
||||
state.update_best_state(&converged.state, converged.final_residual);
|
||||
state.total_iterations += converged.iterations;
|
||||
|
||||
// Finalize diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = state.total_iterations;
|
||||
diag.final_residual = converged.final_residual;
|
||||
diag.best_residual = state.best_residual.unwrap_or(converged.final_residual);
|
||||
diag.converged = true;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.final_solver = Some(state.current_solver.into());
|
||||
diag.solver_switches = state.switch_events.clone();
|
||||
|
||||
// Merge iteration history from child solver if available
|
||||
if let Some(ref child_diag) = converged.diagnostics {
|
||||
diag.iteration_history = child_diag.iteration_history.clone();
|
||||
}
|
||||
|
||||
if self.config.verbose_config.log_residuals {
|
||||
tracing::info!("{}", diag.summary());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
solver = match state.current_solver {
|
||||
@@ -253,7 +327,11 @@ impl FallbackSolver {
|
||||
switch_count = state.switch_count,
|
||||
"Fallback solver converged"
|
||||
);
|
||||
return Ok(converged);
|
||||
|
||||
// Return with diagnostics if verbose mode enabled
|
||||
return Ok(if let Some(d) = diagnostics {
|
||||
ConvergedState { diagnostics: Some(d), ..converged }
|
||||
} else { converged });
|
||||
}
|
||||
Err(SolverError::Timeout { timeout_ms }) => {
|
||||
// Story 4.5 - AC: #4: Return best state on timeout if available
|
||||
@@ -266,7 +344,7 @@ impl FallbackSolver {
|
||||
);
|
||||
return Ok(ConvergedState::new(
|
||||
best_state,
|
||||
0, // iterations not tracked across switches
|
||||
state.total_iterations,
|
||||
best_residual,
|
||||
ConvergenceStatus::TimedOutWithBestState,
|
||||
SimulationMetadata::new(system.input_hash()),
|
||||
@@ -290,11 +368,36 @@ impl FallbackSolver {
|
||||
|
||||
match state.current_solver {
|
||||
CurrentSolver::Newton => {
|
||||
// Get residual from error context (use best known)
|
||||
let residual_at_switch = state.best_residual.unwrap_or(f64::MAX);
|
||||
|
||||
// Newton diverged - switch to Picard (stay there permanently after max switches)
|
||||
if state.switch_count >= self.config.max_fallback_switches {
|
||||
// Max switches reached - commit to Picard permanently
|
||||
state.committed_to_picard = true;
|
||||
let prev_solver = state.current_solver;
|
||||
state.current_solver = CurrentSolver::Picard;
|
||||
|
||||
// Record switch event
|
||||
state.record_switch(
|
||||
prev_solver,
|
||||
state.current_solver,
|
||||
SwitchReason::Divergence,
|
||||
residual_at_switch,
|
||||
);
|
||||
|
||||
// Verbose logging
|
||||
if verbose_enabled && self.config.verbose_config.log_solver_switches {
|
||||
tracing::info!(
|
||||
from = "NewtonRaphson",
|
||||
to = "Picard",
|
||||
reason = "divergence",
|
||||
switch_count = state.switch_count,
|
||||
residual = residual_at_switch,
|
||||
"Solver switch (max switches reached)"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
switch_count = state.switch_count,
|
||||
max_switches = self.config.max_fallback_switches,
|
||||
@@ -303,7 +406,29 @@ impl FallbackSolver {
|
||||
} else {
|
||||
// Switch to Picard
|
||||
state.switch_count += 1;
|
||||
let prev_solver = state.current_solver;
|
||||
state.current_solver = CurrentSolver::Picard;
|
||||
|
||||
// Record switch event
|
||||
state.record_switch(
|
||||
prev_solver,
|
||||
state.current_solver,
|
||||
SwitchReason::Divergence,
|
||||
residual_at_switch,
|
||||
);
|
||||
|
||||
// Verbose logging
|
||||
if verbose_enabled && self.config.verbose_config.log_solver_switches {
|
||||
tracing::info!(
|
||||
from = "NewtonRaphson",
|
||||
to = "Picard",
|
||||
reason = "divergence",
|
||||
switch_count = state.switch_count,
|
||||
residual = residual_at_switch,
|
||||
"Solver switch"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
switch_count = state.switch_count,
|
||||
reason = reason,
|
||||
@@ -337,6 +462,8 @@ impl FallbackSolver {
|
||||
iterations,
|
||||
final_residual,
|
||||
}) => {
|
||||
state.total_iterations += iterations;
|
||||
|
||||
// Non-convergence: check if we should try the other solver
|
||||
if !self.config.fallback_enabled {
|
||||
return Err(SolverError::NonConvergence {
|
||||
@@ -351,14 +478,58 @@ impl FallbackSolver {
|
||||
if state.switch_count >= self.config.max_fallback_switches {
|
||||
// Max switches reached - commit to Picard permanently
|
||||
state.committed_to_picard = true;
|
||||
let prev_solver = state.current_solver;
|
||||
state.current_solver = CurrentSolver::Picard;
|
||||
|
||||
// Record switch event
|
||||
state.record_switch(
|
||||
prev_solver,
|
||||
state.current_solver,
|
||||
SwitchReason::SlowConvergence,
|
||||
final_residual,
|
||||
);
|
||||
|
||||
// Verbose logging
|
||||
if verbose_enabled && self.config.verbose_config.log_solver_switches {
|
||||
tracing::info!(
|
||||
from = "NewtonRaphson",
|
||||
to = "Picard",
|
||||
reason = "slow_convergence",
|
||||
switch_count = state.switch_count,
|
||||
residual = final_residual,
|
||||
"Solver switch (max switches reached)"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
switch_count = state.switch_count,
|
||||
"Max switches reached, committing to Picard permanently"
|
||||
);
|
||||
} else {
|
||||
state.switch_count += 1;
|
||||
let prev_solver = state.current_solver;
|
||||
state.current_solver = CurrentSolver::Picard;
|
||||
|
||||
// Record switch event
|
||||
state.record_switch(
|
||||
prev_solver,
|
||||
state.current_solver,
|
||||
SwitchReason::SlowConvergence,
|
||||
final_residual,
|
||||
);
|
||||
|
||||
// Verbose logging
|
||||
if verbose_enabled && self.config.verbose_config.log_solver_switches {
|
||||
tracing::info!(
|
||||
from = "NewtonRaphson",
|
||||
to = "Picard",
|
||||
reason = "slow_convergence",
|
||||
switch_count = state.switch_count,
|
||||
residual = final_residual,
|
||||
"Solver switch"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
switch_count = state.switch_count,
|
||||
iterations = iterations,
|
||||
@@ -387,7 +558,30 @@ impl FallbackSolver {
|
||||
// Check if residual is low enough to try Newton
|
||||
if final_residual < self.config.return_to_newton_threshold {
|
||||
state.switch_count += 1;
|
||||
let prev_solver = state.current_solver;
|
||||
state.current_solver = CurrentSolver::Newton;
|
||||
|
||||
// Record switch event
|
||||
state.record_switch(
|
||||
prev_solver,
|
||||
state.current_solver,
|
||||
SwitchReason::ReturnToNewton,
|
||||
final_residual,
|
||||
);
|
||||
|
||||
// Verbose logging
|
||||
if verbose_enabled && self.config.verbose_config.log_solver_switches {
|
||||
tracing::info!(
|
||||
from = "Picard",
|
||||
to = "NewtonRaphson",
|
||||
reason = "return_to_newton",
|
||||
switch_count = state.switch_count,
|
||||
residual = final_residual,
|
||||
threshold = self.config.return_to_newton_threshold,
|
||||
"Solver switch (Picard stabilized)"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
switch_count = state.switch_count,
|
||||
final_residual = final_residual,
|
||||
@@ -467,9 +661,12 @@ mod tests {
|
||||
fallback_enabled: false,
|
||||
return_to_newton_threshold: 5e-4,
|
||||
max_fallback_switches: 3,
|
||||
..Default::default()
|
||||
};
|
||||
let solver = FallbackSolver::new(config.clone());
|
||||
assert_eq!(solver.config, config);
|
||||
assert_eq!(solver.config.fallback_enabled, config.fallback_enabled);
|
||||
assert_eq!(solver.config.return_to_newton_threshold, 5e-4);
|
||||
assert_eq!(solver.config.max_fallback_switches, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,8 +9,9 @@ use crate::criteria::ConvergenceCriteria;
|
||||
use crate::jacobian::JacobianMatrix;
|
||||
use crate::metadata::SimulationMetadata;
|
||||
use crate::solver::{
|
||||
apply_newton_step, ConvergedState, ConvergenceStatus, JacobianFreezingConfig, Solver,
|
||||
SolverError, TimeoutConfig,
|
||||
apply_newton_step, ConvergedState, ConvergenceDiagnostics, ConvergenceStatus,
|
||||
IterationDiagnostics, JacobianFreezingConfig, Solver, SolverError, SolverType,
|
||||
TimeoutConfig, VerboseConfig,
|
||||
};
|
||||
use crate::system::System;
|
||||
use entropyk_components::JacobianBuilder;
|
||||
@@ -49,6 +50,8 @@ pub struct NewtonConfig {
|
||||
pub convergence_criteria: Option<ConvergenceCriteria>,
|
||||
/// Jacobian-freezing optimization.
|
||||
pub jacobian_freezing: Option<JacobianFreezingConfig>,
|
||||
/// Verbose mode configuration for diagnostics.
|
||||
pub verbose_config: VerboseConfig,
|
||||
}
|
||||
|
||||
impl Default for NewtonConfig {
|
||||
@@ -68,6 +71,7 @@ impl Default for NewtonConfig {
|
||||
initial_state: None,
|
||||
convergence_criteria: None,
|
||||
jacobian_freezing: None,
|
||||
verbose_config: VerboseConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +95,12 @@ impl NewtonConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables verbose mode for diagnostics.
|
||||
pub fn with_verbose(mut self, config: VerboseConfig) -> Self {
|
||||
self.verbose_config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes the L2 norm of the residual vector.
|
||||
fn residual_norm(residuals: &[f64]) -> f64 {
|
||||
residuals.iter().map(|r| r * r).sum::<f64>().sqrt()
|
||||
@@ -208,10 +218,19 @@ impl Solver for NewtonConfig {
|
||||
fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Initialize diagnostics collection if verbose mode enabled
|
||||
let verbose_enabled = self.verbose_config.enabled && self.verbose_config.is_any_enabled();
|
||||
let mut diagnostics = if verbose_enabled {
|
||||
Some(ConvergenceDiagnostics::with_capacity(self.max_iterations))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
max_iterations = self.max_iterations,
|
||||
tolerance = self.tolerance,
|
||||
line_search = self.line_search,
|
||||
verbose = verbose_enabled,
|
||||
"Newton-Raphson solver starting"
|
||||
);
|
||||
|
||||
@@ -254,6 +273,9 @@ impl Solver for NewtonConfig {
|
||||
let mut jacobian_matrix = JacobianMatrix::zeros(n_equations, n_state);
|
||||
let mut frozen_count: usize = 0;
|
||||
let mut force_recompute: bool = true;
|
||||
|
||||
// Cached condition number (for verbose mode when Jacobian frozen)
|
||||
let mut cached_condition: Option<f64> = None;
|
||||
|
||||
// Pre-compute clipping mask
|
||||
let clipping_mask: Vec<Option<(f64, f64)>> = (0..n_state)
|
||||
@@ -323,6 +345,8 @@ impl Solver for NewtonConfig {
|
||||
true
|
||||
};
|
||||
|
||||
let jacobian_frozen_this_iter = !should_recompute;
|
||||
|
||||
if should_recompute {
|
||||
// Fresh Jacobian assembly (in-place update)
|
||||
jacobian_builder.clear();
|
||||
@@ -350,6 +374,19 @@ impl Solver for NewtonConfig {
|
||||
|
||||
frozen_count = 0;
|
||||
force_recompute = false;
|
||||
|
||||
// Compute and cache condition number if verbose mode enabled
|
||||
if verbose_enabled && self.verbose_config.log_jacobian_condition {
|
||||
let cond = jacobian_matrix.estimate_condition_number();
|
||||
cached_condition = cond;
|
||||
if let Some(c) = cond {
|
||||
tracing::info!(iteration, condition_number = c, "Jacobian condition number");
|
||||
if c > 1e10 {
|
||||
tracing::warn!(iteration, condition_number = c, "Ill-conditioned Jacobian detected (κ > 1e10)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(iteration, "Fresh Jacobian computed");
|
||||
} else {
|
||||
frozen_count += 1;
|
||||
@@ -391,6 +428,13 @@ impl Solver for NewtonConfig {
|
||||
|
||||
previous_norm = current_norm;
|
||||
current_norm = Self::residual_norm(&residuals);
|
||||
|
||||
// Compute delta norm for diagnostics
|
||||
let delta_norm: f64 = state.iter()
|
||||
.zip(prev_iteration_state.iter())
|
||||
.map(|(s, p)| (s - p).powi(2))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
|
||||
if current_norm < best_residual {
|
||||
best_state.copy_from_slice(&state);
|
||||
@@ -409,6 +453,30 @@ impl Solver for NewtonConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Verbose mode: Log iteration residuals
|
||||
if verbose_enabled && self.verbose_config.log_residuals {
|
||||
tracing::info!(
|
||||
iteration,
|
||||
residual_norm = current_norm,
|
||||
delta_norm = delta_norm,
|
||||
alpha = alpha,
|
||||
jacobian_frozen = jacobian_frozen_this_iter,
|
||||
"Newton iteration"
|
||||
);
|
||||
}
|
||||
|
||||
// Collect iteration diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.push_iteration(IterationDiagnostics {
|
||||
iteration,
|
||||
residual_norm: current_norm,
|
||||
delta_norm,
|
||||
alpha: Some(alpha),
|
||||
jacobian_frozen: jacobian_frozen_this_iter,
|
||||
jacobian_condition: cached_condition,
|
||||
});
|
||||
}
|
||||
|
||||
tracing::debug!(iteration, residual_norm = current_norm, alpha, "Newton iteration complete");
|
||||
|
||||
// Check convergence
|
||||
@@ -420,10 +488,29 @@ impl Solver for NewtonConfig {
|
||||
} else {
|
||||
ConvergenceStatus::Converged
|
||||
};
|
||||
|
||||
// Finalize diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = iteration;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = true;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.jacobian_condition_final = cached_condition;
|
||||
diag.final_solver = Some(SolverType::NewtonRaphson);
|
||||
|
||||
if self.verbose_config.log_residuals {
|
||||
tracing::info!("{}", diag.summary());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(iterations = iteration, final_residual = current_norm, "Converged (criteria)");
|
||||
return Ok(ConvergedState::with_report(
|
||||
let result = ConvergedState::with_report(
|
||||
state, iteration, current_norm, status, report, SimulationMetadata::new(system.input_hash()),
|
||||
));
|
||||
);
|
||||
return Ok(if let Some(d) = diagnostics {
|
||||
ConvergedState { diagnostics: Some(d), ..result }
|
||||
} else { result });
|
||||
}
|
||||
false
|
||||
} else {
|
||||
@@ -436,10 +523,29 @@ impl Solver for NewtonConfig {
|
||||
} else {
|
||||
ConvergenceStatus::Converged
|
||||
};
|
||||
|
||||
// Finalize diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = iteration;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = true;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.jacobian_condition_final = cached_condition;
|
||||
diag.final_solver = Some(SolverType::NewtonRaphson);
|
||||
|
||||
if self.verbose_config.log_residuals {
|
||||
tracing::info!("{}", diag.summary());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(iterations = iteration, final_residual = current_norm, "Converged");
|
||||
return Ok(ConvergedState::new(
|
||||
let result = ConvergedState::new(
|
||||
state, iteration, current_norm, status, SimulationMetadata::new(system.input_hash()),
|
||||
));
|
||||
);
|
||||
return Ok(if let Some(d) = diagnostics {
|
||||
ConvergedState { diagnostics: Some(d), ..result }
|
||||
} else { result });
|
||||
}
|
||||
|
||||
if let Some(err) = self.check_divergence(current_norm, previous_norm, &mut divergence_count) {
|
||||
@@ -448,6 +554,28 @@ impl Solver for NewtonConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Non-convergence: dump diagnostics if enabled
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = self.max_iterations;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = false;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.jacobian_condition_final = cached_condition;
|
||||
diag.final_solver = Some(SolverType::NewtonRaphson);
|
||||
|
||||
if self.verbose_config.dump_final_state {
|
||||
diag.final_state = Some(state.clone());
|
||||
let json_output = diag.dump_diagnostics(self.verbose_config.output_format);
|
||||
tracing::warn!(
|
||||
iterations = self.max_iterations,
|
||||
final_residual = current_norm,
|
||||
"Non-convergence diagnostics:\n{}",
|
||||
json_output
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::warn!(max_iterations = self.max_iterations, final_residual = current_norm, "Did not converge");
|
||||
Err(SolverError::NonConvergence {
|
||||
iterations: self.max_iterations,
|
||||
|
||||
@@ -7,7 +7,10 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use crate::criteria::ConvergenceCriteria;
|
||||
use crate::metadata::SimulationMetadata;
|
||||
use crate::solver::{ConvergedState, ConvergenceStatus, Solver, SolverError, TimeoutConfig};
|
||||
use crate::solver::{
|
||||
ConvergedState, ConvergenceDiagnostics, ConvergenceStatus, IterationDiagnostics, Solver,
|
||||
SolverError, SolverType, TimeoutConfig, VerboseConfig,
|
||||
};
|
||||
use crate::system::System;
|
||||
|
||||
/// Configuration for the Sequential Substitution (Picard iteration) solver.
|
||||
@@ -38,6 +41,8 @@ pub struct PicardConfig {
|
||||
pub initial_state: Option<Vec<f64>>,
|
||||
/// Multi-circuit convergence criteria.
|
||||
pub convergence_criteria: Option<ConvergenceCriteria>,
|
||||
/// Verbose mode configuration for diagnostics.
|
||||
pub verbose_config: VerboseConfig,
|
||||
}
|
||||
|
||||
impl Default for PicardConfig {
|
||||
@@ -54,6 +59,7 @@ impl Default for PicardConfig {
|
||||
previous_residual: None,
|
||||
initial_state: None,
|
||||
convergence_criteria: None,
|
||||
verbose_config: VerboseConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +84,12 @@ impl PicardConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables verbose mode for diagnostics.
|
||||
pub fn with_verbose(mut self, config: VerboseConfig) -> Self {
|
||||
self.verbose_config = config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Computes the residual norm (L2 norm of the residual vector).
|
||||
fn residual_norm(residuals: &[f64]) -> f64 {
|
||||
residuals.iter().map(|r| r * r).sum::<f64>().sqrt()
|
||||
@@ -194,12 +206,21 @@ impl Solver for PicardConfig {
|
||||
fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
// Initialize diagnostics collection if verbose mode enabled
|
||||
let verbose_enabled = self.verbose_config.enabled && self.verbose_config.is_any_enabled();
|
||||
let mut diagnostics = if verbose_enabled {
|
||||
Some(ConvergenceDiagnostics::with_capacity(self.max_iterations))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
max_iterations = self.max_iterations,
|
||||
tolerance = self.tolerance,
|
||||
relaxation_factor = self.relaxation_factor,
|
||||
divergence_threshold = self.divergence_threshold,
|
||||
divergence_patience = self.divergence_patience,
|
||||
verbose = verbose_enabled,
|
||||
"Sequential Substitution (Picard) solver starting"
|
||||
);
|
||||
|
||||
@@ -328,6 +349,13 @@ impl Solver for PicardConfig {
|
||||
|
||||
previous_norm = current_norm;
|
||||
current_norm = Self::residual_norm(&residuals);
|
||||
|
||||
// Compute delta norm for diagnostics
|
||||
let delta_norm: f64 = state.iter()
|
||||
.zip(prev_iteration_state.iter())
|
||||
.map(|(s, p)| (s - p).powi(2))
|
||||
.sum::<f64>()
|
||||
.sqrt();
|
||||
|
||||
// Update best state if residual improved (Story 4.5 - AC: #2)
|
||||
if current_norm < best_residual {
|
||||
@@ -340,6 +368,29 @@ impl Solver for PicardConfig {
|
||||
);
|
||||
}
|
||||
|
||||
// Verbose mode: Log iteration residuals
|
||||
if verbose_enabled && self.verbose_config.log_residuals {
|
||||
tracing::info!(
|
||||
iteration,
|
||||
residual_norm = current_norm,
|
||||
delta_norm = delta_norm,
|
||||
relaxation_factor = self.relaxation_factor,
|
||||
"Picard iteration"
|
||||
);
|
||||
}
|
||||
|
||||
// Collect iteration diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.push_iteration(IterationDiagnostics {
|
||||
iteration,
|
||||
residual_norm: current_norm,
|
||||
delta_norm,
|
||||
alpha: None, // Picard doesn't use line search
|
||||
jacobian_frozen: false, // Picard doesn't use Jacobian
|
||||
jacobian_condition: None, // No Jacobian in Picard
|
||||
});
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
iteration = iteration,
|
||||
residual_norm = current_norm,
|
||||
@@ -352,20 +403,37 @@ impl Solver for PicardConfig {
|
||||
let report =
|
||||
criteria.check(&state, Some(&prev_iteration_state), &residuals, system);
|
||||
if report.is_globally_converged() {
|
||||
// Finalize diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = iteration;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = true;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.final_solver = Some(SolverType::SequentialSubstitution);
|
||||
|
||||
if self.verbose_config.log_residuals {
|
||||
tracing::info!("{}", diag.summary());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
iterations = iteration,
|
||||
final_residual = current_norm,
|
||||
relaxation_factor = self.relaxation_factor,
|
||||
"Sequential Substitution converged (criteria)"
|
||||
);
|
||||
return Ok(ConvergedState::with_report(
|
||||
let result = ConvergedState::with_report(
|
||||
state,
|
||||
iteration,
|
||||
current_norm,
|
||||
ConvergenceStatus::Converged,
|
||||
report,
|
||||
SimulationMetadata::new(system.input_hash()),
|
||||
));
|
||||
);
|
||||
return Ok(if let Some(d) = diagnostics {
|
||||
ConvergedState { diagnostics: Some(d), ..result }
|
||||
} else { result });
|
||||
}
|
||||
false
|
||||
} else {
|
||||
@@ -373,19 +441,36 @@ impl Solver for PicardConfig {
|
||||
};
|
||||
|
||||
if converged {
|
||||
// Finalize diagnostics
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = iteration;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = true;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.final_solver = Some(SolverType::SequentialSubstitution);
|
||||
|
||||
if self.verbose_config.log_residuals {
|
||||
tracing::info!("{}", diag.summary());
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
iterations = iteration,
|
||||
final_residual = current_norm,
|
||||
relaxation_factor = self.relaxation_factor,
|
||||
"Sequential Substitution converged"
|
||||
);
|
||||
return Ok(ConvergedState::new(
|
||||
let result = ConvergedState::new(
|
||||
state,
|
||||
iteration,
|
||||
current_norm,
|
||||
ConvergenceStatus::Converged,
|
||||
SimulationMetadata::new(system.input_hash()),
|
||||
));
|
||||
);
|
||||
return Ok(if let Some(d) = diagnostics {
|
||||
ConvergedState { diagnostics: Some(d), ..result }
|
||||
} else { result });
|
||||
}
|
||||
|
||||
// Check divergence (AC: #5)
|
||||
@@ -401,6 +486,27 @@ impl Solver for PicardConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// Non-convergence: dump diagnostics if enabled
|
||||
if let Some(ref mut diag) = diagnostics {
|
||||
diag.iterations = self.max_iterations;
|
||||
diag.final_residual = current_norm;
|
||||
diag.best_residual = best_residual;
|
||||
diag.converged = false;
|
||||
diag.timing_ms = start_time.elapsed().as_millis() as u64;
|
||||
diag.final_solver = Some(SolverType::SequentialSubstitution);
|
||||
|
||||
if self.verbose_config.dump_final_state {
|
||||
diag.final_state = Some(state.clone());
|
||||
let json_output = diag.dump_diagnostics(self.verbose_config.output_format);
|
||||
tracing::warn!(
|
||||
iterations = self.max_iterations,
|
||||
final_residual = current_norm,
|
||||
"Non-convergence diagnostics:\n{}",
|
||||
json_output
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Max iterations exceeded
|
||||
tracing::warn!(
|
||||
max_iterations = self.max_iterations,
|
||||
|
||||
Reference in New Issue
Block a user