chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
@@ -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