# Story 3.1: System Graph Structure

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## 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<dyn Component>` 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<f64>` - 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<N, E, Ty, Ix>` - 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)
