# Story 3.1: System Graph Structure Status: done ## Story As a system modeler, I want edges to index the solver's state vector, so that P and h unknowns assemble into the Jacobian. ## Acceptance Criteria 1. **Edges as State Indices** (AC: #1) - [x] Given components with ports, when adding to System graph, edges serve as indices into solver's state vector - [x] Each flow edge represents P and h unknowns (2 indices per edge) - [x] State vector layout: `[P_edge0, h_edge0, P_edge1, h_edge1, ...]` or equivalent documented layout 2. **Graph Traversal for Jacobian** (AC: #2) - [x] Solver traverses graph to assemble Jacobian - [x] Components receive state slice indices via edge→state mapping - [x] JacobianBuilder entries use correct (row, col) from graph topology 3. **Cycle Detection and Validation** (AC: #3) - [x] Cycles are detected (refrigeration circuits form cycles - expected) - [x] Topology is validated (no dangling nodes, consistent flow direction) - [x] Clear error when topology is invalid ## Tasks / Subtasks - [x] Create solver crate (AC: #1) - [x] Add `crates/solver` to workspace Cargo.toml - [x] Create Cargo.toml with deps: entropyk-core, entropyk-components, petgraph, thiserror - [x] Create lib.rs with module structure (system, graph) - [x] Implement System graph structure (AC: #1) - [x] Use petgraph `Graph` with nodes = components, edges = flow connections - [x] Node weight: `Box` or component handle - [x] Edge weight: `FlowEdge { state_index_p: usize, state_index_h: usize }` or similar - [x] Build edge→state index mapping when graph is finalized - [x] State vector indexing (AC: #1) - [x] `state_vector_len() -> usize` = 2 * edge_count (P and h per edge) - [x] `edge_state_indices(edge_id) -> (usize, usize)` for P and h columns - [x] Document layout in rustdoc - [x] Graph traversal for Jacobian assembly (AC: #2) - [x] `traverse_for_jacobian()` or iterator over (component, edge_indices) - [x] Components receive state and write to JacobianBuilder with correct col indices - [x] Integrate with existing `Component::jacobian_entries(state, jacobian)` - [x] Cycle detection and validation (AC: #3) - [x] Use `petgraph::algo::is_cyclic_directed` for cycle detection - [x] Validate: refrigeration cycles are expected; document semantics - [x] Validate: no isolated nodes, all ports connected - [x] Return `Result<(), TopologyError>` on invalid topology - [x] Add tests - [x] Test: simple cycle (4 nodes, 4 edges) builds correctly - [x] Test: state vector length = 2 * edge_count - [x] Test: edge indices are contiguous and unique - [x] Test: cycle detection identifies cyclic graph - [x] Test: invalid topology (dangling node) returns error ## Dev Notes ### Epic Context **Epic 3: System Topology (Graph)** - Enable component assembly via Ports and manage multi-circuits with thermal coupling. FR9–FR13 map to `crates/solver/src/system.rs`. **Story Dependencies:** - Epic 1 (Component trait, Ports, State machine) - in progress, 1-8 in review - No dependency on Epic 2 (fluids) for this story - topology only ### Architecture Context **System Topology (FR9–FR13):** - Location: `crates/solver/src/system.rs` - petgraph for graph topology representation (architecture line 89, 147) - `solver → core + fluids + components` (components required) - Jacobian entries assembled by solver (architecture line 760) **Workspace:** - `crates/solver` is currently commented out in root Cargo.toml: `# "crates/solver", # Will be added in future stories` - This story CREATES the solver crate **Component Trait (from components):** ```rust pub trait Component { fn compute_residuals(&self, state: &SystemState, residuals: &mut ResidualVector) -> Result<(), ComponentError>; fn jacobian_entries(&self, state: &SystemState, jacobian: &mut JacobianBuilder) -> Result<(), ComponentError>; fn n_equations(&self) -> usize; fn get_ports(&self) -> &[ConnectedPort]; } ``` - `SystemState = Vec` - state vector - `JacobianBuilder` has `add_entry(row, col, value)` - col = state index - Components need to know which state indices map to their ports ### Technical Requirements **Graph Model:** - **Nodes**: Components (or component handles). Each node = one `dyn Component`. - **Edges**: Flow connections between component ports. Direction = flow direction (e.g., compressor outlet → condenser inlet). - **Edge weight**: Must store `(state_index_p, state_index_h)` so solver can map state vector to edges. **State Vector Layout:** - Option A: `[P_0, h_0, P_1, h_1, ...]` - 2 per edge, edge order from graph - Option B: Separate P and h vectors - less common for Newton - Recommended: Option A for simplicity; document in `System::state_layout()` **Cycle Semantics:** - Refrigeration circuits ARE cycles (compressor → condenser → valve → evaporator → compressor) - `is_cyclic_directed` returns true for valid refrigeration topology - "Validated" = topology is well-formed (connected, no illegal configs), not "no cycles" - Port `ConnectionError::CycleDetected` may refer to a different concept (e.g., connection graph cycles during build) - clarify in implementation **petgraph API:** - `Graph` - use `Directed` for flow direction - `add_node(weight)`, `add_edge(a, b, weight)` - `petgraph::algo::is_cyclic_directed(&graph) -> bool` - `NodeIndex`, `EdgeIndex` for stable references ### Library/Framework Requirements **petgraph:** - Version: 0.6.x (latest stable, check crates.io) - Docs: https://docs.rs/petgraph/ - `use petgraph::graph::Graph` and `petgraph::algo::is_cyclic_directed` **Dependencies (solver Cargo.toml):** ```toml [dependencies] entropyk-core = { path = "../core" } entropyk-components = { path = "../components" } petgraph = "0.6" thiserror = "1.0" ``` ### File Structure Requirements **New files:** - `crates/solver/Cargo.toml` - `crates/solver/src/lib.rs` - re-exports - `crates/solver/src/system.rs` - System struct, Graph, state indexing - `crates/solver/src/graph.rs` (optional) - graph building helpers - `crates/solver/src/error.rs` (optional) - TopologyError **Modified files:** - Root `Cargo.toml` - uncomment `crates/solver` in workspace members ### Testing Requirements **Required Tests:** - `test_simple_cycle_builds` - 4 components, 4 edges, graph builds - `test_state_vector_length` - len = 2 * edge_count - `test_edge_indices_contiguous` - indices 0..2n for n edges - `test_cycle_detected` - cyclic graph, is_cyclic_directed true - `test_dangling_node_error` - node with no edges returns TopologyError - `test_traverse_components` - traversal yields all components with correct edge indices **Integration:** - Build a minimal cycle (e.g., 2 components, 2 edges) and verify state layout ### Project Structure Notes **Alignment:** - Architecture specifies `crates/solver/src/system.rs` for FR9–FR13 - matches - petgraph from architecture - matches - Component trait from Epic 1 - get_ports() provides port info for graph building **Note:** Story 3.2 (Port Compatibility Validation) will add connection-time checks. This story focuses on graph structure and state indexing once components are added. ### References - **Epic 3 Story 3.1:** [Source: planning-artifacts/epics.md#Story 3.1] - **Architecture System Topology:** [Source: planning-artifacts/architecture.md - FR9-FR13, system.rs] - **Architecture petgraph:** [Source: planning-artifacts/architecture.md - line 89, 147] - **Component trait:** [Source: crates/components/src/lib.rs] - **Port types:** [Source: crates/components/src/port.rs] - **JacobianBuilder:** [Source: crates/components/src/lib.rs - JacobianBuilder] - **petgraph is_cyclic_directed:** https://docs.rs/petgraph/latest/petgraph/algo/fn.is_cyclic_directed.html ## Change Log - 2026-02-15: Story 3.1 implementation complete. Created crates/solver with System graph (petgraph), FlowEdge, state indexing, traverse_for_jacobian, cycle detection, TopologyError. All ACs satisfied, 7 tests pass. - 2026-02-15: Code review fixes. Added state_layout(), compute_residuals bounds check, robust test_dangling_node_error, TopologyError #[allow(dead_code)], removed unused deps (entropyk-core, approx), added test_state_layout_integration and test_compute_residuals_bounds_check. 9 tests pass. ## Dev Agent Record ### Agent Model Used {{agent_model_name_version}} ### Debug Log References ### Completion Notes List - Created `crates/solver` with petgraph-based System graph - FlowEdge stores (state_index_p, state_index_h) per edge - State vector layout: [P_0, h_0, P_1, h_1, ...] documented in rustdoc and state_layout() - traverse_for_jacobian() yields (component, edge_indices) for Jacobian assembly - TopologyError::IsolatedNode for dangling nodes; refrigeration cycles expected (is_cyclic) - All 9 tests pass (core, components, solver); no fluids dependency - Code review: state_layout(), bounds check in compute_residuals, robust dangling-node test, integration test ### File List - crates/solver/Cargo.toml (new) - crates/solver/src/lib.rs (new) - crates/solver/src/error.rs (new) - crates/solver/src/graph.rs (new) - crates/solver/src/system.rs (new) - Cargo.toml (modified: uncommented crates/solver in workspace) - _bmad-output/implementation-artifacts/sprint-status.yaml (modified: 3-1 in-progress → review)