13 KiB
Story 4.1: Solver Trait Abstraction
Status: done
Story
As a numerical developer, I want a generic Solver trait, so that strategies are interchangeable.
Acceptance Criteria
-
Solver Trait Defined (AC: #1)
- Given a system of equations represented by
System - When implementing a solver strategy
- Then it must implement the common
Solvertrait - And the trait provides
solve()andwith_timeout()methods - And the trait is object-safe for dynamic dispatch
- Given a system of equations represented by
-
Zero-Cost Abstraction via Enum Dispatch (AC: #2)
- Given multiple solver strategies (Newton-Raphson, Sequential Substitution)
- When selecting a strategy at runtime
- Then an enum
SolverStrategydispatches to the correct implementation - And there is no vtable overhead (monomorphization via enum)
- And the pattern matches the architecture decision for static polymorphism
-
Timeout Support (AC: #3)
- Given a solver with a configured timeout
- When the solver exceeds the time budget
- Then it stops immediately and returns
SolverError::Timeout - And the timeout is configurable via
with_timeout(Duration)
-
Error Handling (AC: #4)
- Given a solver that fails to converge
- When checking the result
- Then it returns
SolverError::NonConvergencewith iteration count and final residual - And all error variants are documented and follow the
thiserrorpattern
Tasks / Subtasks
-
Define
Solvertrait incrates/solver/src/solver.rs(AC: #1)- Create new module
solver.rswithSolvertrait - Define
solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> - Define
with_timeout(self, timeout: Duration) -> Self(builder pattern) - Ensure trait is object-safe (no generic methods, no
Selfin return types) - Add rustdoc with KaTeX equations for convergence criteria
- Create new module
-
Define
SolverErrorenum (AC: #4)- Add
NonConvergence { iterations: usize, final_residual: f64 } - Add
Timeout { timeout_ms: u64 } - Add
Divergence { reason: String } - Add
InvalidSystem { message: String } - Use
thiserror::Errorderive
- Add
-
Define
ConvergedStatestruct (AC: #1)- Store final state vector
Vec<f64> - Store iteration count
usize - Store final residual norm
f64 - Store convergence status
ConvergenceStatusenum
- Store final state vector
-
Define
SolverStrategyenum (AC: #2)NewtonRaphson(NewtonConfig)variantSequentialSubstitution(PicardConfig)variant- Implement
SolverforSolverStrategywith match dispatch - Add
Defaultimpl returning Newton-Raphson
-
Define configuration structs (AC: #2)
NewtonConfigwith max_iterations, tolerance, line_search flagPicardConfigwith max_iterations, tolerance, relaxation_factor- Both implement
Defaultwith sensible defaults (100 iterations, 1e-6 tolerance)
-
Add timeout infrastructure (AC: #3)
- Add
timeout: Option<Duration>to config structs - Add
with_timeout()builder method - Note: Actual timeout enforcement will be in Story 4.2/4.3; this story only defines the API
- Add
-
Update
crates/solver/src/lib.rs(AC: #1)- Add
pub mod solver; - Re-export
Solver,SolverError,SolverStrategy,ConvergedState - Re-export
NewtonConfig,PicardConfig
- Add
-
Tests (AC: #1, #2, #3, #4)
- Test
Solvertrait object safety (Box<dyn Solver>compiles) - Test
SolverStrategy::default()returns Newton-Raphson - Test
with_timeout()returns modified config - Test error variants have correct Display messages
- Test
ConvergedStatefields are accessible
- Test
Dev Notes
Epic Context
Epic 4: Intelligent Solver Engine — Solve any system with < 1s guarantee, Newton-Raphson ↔ Sequential Substitution fallback.
Story Dependencies:
- Epic 1 (Component trait) — done;
Componenttrait providescompute_residuals,jacobian_entries - Epic 2 (Fluid properties) — done; fluid backends available
- Epic 3 (System topology) — done;
Systemstruct with graph, state vector, residual/Jacobian assembly - Story 4.2 (Newton-Raphson) — will implement
NewtonRaphsonsolver - Story 4.3 (Sequential Substitution) — will implement
SequentialSubstitutionsolver - Story 4.4 (Intelligent Fallback) — will use
SolverStrategyenum for auto-switching
FRs covered: FR14 (Newton-Raphson), FR15 (Sequential Substitution), FR17 (timeout), FR18 (best state on timeout)
Architecture Context
Technical Stack:
- Rust,
thiserrorfor error handling,tracingfor observability - No new external crates; use
std::time::Durationfor timeout nalgebrawill be used in Story 4.2 for linear algebra (not this story)
Code Structure:
crates/solver/src/solver.rs— new file forSolvertrait,SolverError,SolverStrategy, configscrates/solver/src/lib.rs— re-exportscrates/solver/src/system.rs— existingSystemstruct (no changes needed)
Relevant Architecture Decisions:
- Solver Architecture: Trait-based static polymorphism with enum dispatch [Source: architecture.md]
- Zero-cost abstraction: Enum dispatch avoids vtable overhead while allowing runtime selection
- Error Handling: Centralized error enum with
thiserror[Source: architecture.md] - No panic policy: All errors return
Result<T, SolverError>
Developer Context
Existing Implementation:
- System struct (
crates/solver/src/system.rs):compute_residuals(&self, state: &StateSlice, residuals: &mut ResidualVector)assemble_jacobian(&self, state: &StateSlice, jacobian: &mut JacobianBuilder)state_vector_len(),edge_count(),node_count()finalize()must be called before solving
- Component trait (
crates/components/src/lib.rs):compute_residuals,jacobian_entries,n_equations,get_ports- Object-safe, used via
Box<dyn Component>
- JacobianBuilder (
crates/components/src/lib.rs):add_entry(row, col, value),entries(),clear()
- TopologyError (
crates/solver/src/error.rs):- Pattern for error enum with
thiserror
- Pattern for error enum with
Design Decisions:
- Trait vs Enum: The architecture specifies enum dispatch for zero-cost abstraction. The
Solvertrait defines the interface, andSolverStrategyenum provides the dispatch mechanism. Both are needed. - Object Safety: The
Solvertrait must be object-safe to allowBox<dyn Solver>for advanced use cases (e.g., user-provided custom solvers). This means:- No generic methods
- No
Selfin return types - No methods that take
selfby value (use&selfor&mut self)
- Timeout API: This story defines the
with_timeout()API. Actual enforcement requiresstd::time::Instantchecks in the solve loop (Story 4.2/4.3). - ConvergedState vs SystemState:
ConvergedStateis the result type returned by solvers, containing metadata.SystemState(alias forVec<f64>) is the state vector used during solving.
Technical Requirements
Solver Trait:
pub trait Solver {
/// Solve the system of equations.
///
/// # Errors
///
/// Returns `SolverError::NonConvergence` if max iterations exceeded.
/// Returns `SolverError::Timeout` if time budget exceeded.
fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError>;
/// Set a timeout for the solver.
///
/// If the solver exceeds this duration, it returns `SolverError::Timeout`.
fn with_timeout(self, timeout: Duration) -> Self;
}
SolverStrategy Enum:
pub enum SolverStrategy {
NewtonRaphson(NewtonConfig),
SequentialSubstitution(PicardConfig),
}
impl Solver for SolverStrategy {
fn solve(&mut self, system: &mut System) -> Result<ConvergedState, SolverError> {
match self {
Self::NewtonRaphson(cfg) => cfg.solve(system), // Story 4.2
Self::SequentialSubstitution(cfg) => cfg.solve(system), // Story 4.3
}
}
// ...
}
Error Handling:
- All errors use
thiserror::Errorderive - Error messages are clear and actionable
- No panics in solver code paths
Architecture Compliance
- NewType pattern: Use
Pressure,Temperaturefrom core where applicable (not directly in Solver trait, but in convergence criteria) - No bare f64 in public API where physical meaning exists
- tracing: Add
tracing::info!for solver start/end,tracing::debug!for iterations - Result<T, E>: All fallible operations return
Result - approx: Use for convergence checks in implementations (Story 4.2/4.3)
Library/Framework Requirements
- std::time::Duration — timeout configuration
- thiserror — error enum derive (already in solver Cargo.toml)
- tracing — structured logging (already used in solver)
File Structure Requirements
New files:
crates/solver/src/solver.rs— Solver trait, SolverError, SolverStrategy, configs, ConvergedState
Modified files:
crates/solver/src/lib.rs— addpub mod solver;and re-exports
Tests:
- Unit tests in
solver.rsmodule (trait object safety, enum dispatch, error messages) - Integration tests will be in Story 4.2/4.3 when actual solvers are implemented
Testing Requirements
- Trait object safety:
let solver: Box<dyn Solver> = Box::new(NewtonConfig::default());compiles - Enum dispatch:
SolverStrategy::default().solve(&mut system)dispatches correctly - Error Display: All error variants have meaningful messages
- Timeout builder:
config.with_timeout(Duration::from_millis(100))returns modified config - Default configs:
NewtonConfig::default()andPicardConfig::default()provide sensible values
Previous Story Intelligence (3.5)
- Zero-flow regularization uses
MIN_MASS_FLOW_REGULARIZATION_KG_Sfrom core - Components handle
OperationalState::Offwith zero mass flow - Solver must handle systems with Off components (residuals/Jacobian already finite)
- Test pattern:
assert!(residuals.iter().all(|r| r.is_finite()))
Git Intelligence
Recent commits show:
- Epic 3 completion (multi-circuit, thermal coupling, zero-flow)
- Component framework mature (Compressor, HeatExchanger, Pipe, etc.)
- Fluid backends ready (CoolProp, Tabular, Cache)
- Ready for solver implementation
Project Context Reference
- FR14: [Source: epics.md — System can solve equations using Newton-Raphson method]
- FR15: [Source: epics.md — System can solve equations using Sequential Substitution (Picard) method]
- FR17: [Source: epics.md — Solver respects configurable time budget (timeout)]
- FR18: [Source: epics.md — On timeout, solver returns best known state with NonConverged status]
- Solver Architecture: [Source: architecture.md — Trait-based static polymorphism with enum dispatch]
- Error Handling: [Source: architecture.md — Centralized error enum with thiserror]
- Component Trait: [Source: crates/components/src/lib.rs — Object-safe trait pattern]
Story Completion Status
- Status: review
- Completion note: Story context created. Solver trait, SolverError, SolverStrategy enum, and config structs implemented. Actual solver algorithms in Stories 4.2 and 4.3.
Change Log
- 2026-02-18: Story 4.1 created from create-story workflow. Epic 4 kickoff. Ready for dev.
- 2026-02-18: Story 4.1 implemented. All tasks complete. 74 tests pass (63 unit + 4 integration + 7 doc-tests). Status → review.
- 2026-02-18: Code review completed. Fixed: (1) doc-test
rust,ignore→rust,no_run(now compiles), (2) added 3 dispatch tests forSolverStrategy::solve(), (3) fixed broken doc link in error.rs. 78 tests pass (66 unit + 4 integration + 8 doc-tests). Status → done.
Dev Agent Record
Agent Model Used
claude-sonnet-4-5 (via Cline)
Debug Log References
- Doc-test failures on first run:
with_timeoutnot in scope in doc examples. Fixed by addinguse entropyk_solver::solver::Solver;to both doc examples.
Completion Notes List
- ✅ Created
crates/solver/src/solver.rswithSolvertrait,SolverError(4 variants),ConvergedState,ConvergenceStatus,SolverStrategyenum,NewtonConfig,PicardConfig - ✅
Solvertrait is object-safe:solve(&mut self, system: &mut System)uses&mut self;with_timeoutiswhere Self: Sizedso it is excluded from the vtable - ✅
SolverStrategyenum provides zero-cost static dispatch viamatch(no vtable) - ✅
SolverStrategy::default()returnsNewtonRaphson(NewtonConfig::default()) - ✅
with_timeout()builder pattern implemented on all three types (NewtonConfig,PicardConfig,SolverStrategy) - ✅
SolverErrorusesthiserror::Errorderive with 4 variants:NonConvergence,Timeout,Divergence,InvalidSystem - ✅
NewtonConfigandPicardConfigstubs returnSolverError::InvalidSystem(full implementation in Stories 4.2/4.3) - ✅
tracing::info!added to solver dispatch and stub implementations - ✅ Updated
crates/solver/src/lib.rswithpub mod solver;and all re-exports - ✅ 19 unit tests covering all ACs; 74 total tests pass with 0 regressions
- ✅ Code review: 3 issues fixed (doc-test, dispatch tests, doc link); 78 tests now pass
File List
crates/solver/src/solver.rs(new)crates/solver/src/lib.rs(modified)crates/solver/src/error.rs(modified — doc link fix)