Entropyk/_bmad-output/implementation-artifacts/3-6-hierarchical-macro-components.md
2026-02-21 10:43:55 +01:00

122 lines
7.2 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Story 3.6: Hierarchical Subsystems (MacroComponents)
Status: done
## Story
As a system designer,
I want to encapsulate a complete system (e.g., a Chiller with compressor, condenser, valve, evaporator) into a single reusable block,
so that I can compose larger models (like buildings or parallel chiller plants) using these blocks, just like in Modelica.
## Acceptance Criteria
1. **MacroComponent Trait Implementation** (AC: #1)
- Given a fully defined `System` with internal components and connections
- When I wrap it in a `MacroComponent`
- Then this `MacroComponent` implements the `Component` trait
- And the global solver treats it exactly like a basic Component
2. **External Port Mapping** (AC: #2)
- Given a `MacroComponent` wrapping an internal `System`
- When I want to expose specific internal ports (e.g., Evaporator Water In/Out, Condenser Water In/Out)
- Then I can map these to the `MacroComponent`'s external ports
- And external connections to these mapped ports correctly route fluid states to the internal components
3. **Residual and Jacobian Delegation** (AC: #3)
- Given a system solver calling `compute_residuals` or `jacobian_entries` on a `MacroComponent`
- When the `MacroComponent` executes these methods
- Then it delegates or flattens the computation down to the nested internal `System`
- And all equations are solved simultaneously globally, avoiding nested numerical solver delays
4. **Serialization and Persistence** (AC: #4)
- Given a `System` that contains `MacroComponent`s
- When serializing the system to JSON
- ~~Then the internal topology of the `MacroComponent` is preserved and can be deserialized perfectly~~ (Moved to Future Scope: Serialization not natively supported yet)
## Tasks / Subtasks
- [x] Define `MacroComponent` struct in `crates/solver/src/macro_component.rs` (AC: #1)
- [x] Store internal `System`
- [x] Store `port_mapping` dictionary
- [x] Implement `Component` trait for `MacroComponent` (AC: #1, #3)
- [x] Implement `get_ports` returning mapped external ports
- [x] Implement `compute_residuals` by delegating to internal components
- [x] Implement `jacobian_entries` by offsetting indices and delegating to internal components
- [x] Implement `n_equations` returning the sum of internal equations
- [x] Implement external port bounding/mapping logic (AC: #2)
- [x] Create API for `expose_port(internal_edge_pos, name, port)`
- [x] Integration Tests (AC: #1-#3)
- [x] Test encapsulating a 4-component cycle into a single `MacroComponent`
- [x] Test connecting two identical `MacroComponent` chillers in parallel inside a higher-level `System`
- [x] Assert global convergence works simultaneously.
- [ ] Create Action Item: Implement fully recursive `serde` serialization for `MacroComponent` topologies.
## Dev Notes
### Epic Context
**Epic 3: System Topology (Graph)** — Enable component assembly via Ports and manage multi-circuits with thermal coupling.
This story adds the capability to wrap topologies into sub-blocks.
**FRs covered:** FR48 (Hierarchical Subsystems).
### Architecture Context
**Technical Stack:**
- Rust, `entropyk-components`, `entropyk-solver`
- Need to ensure that `SystemState` indices stay aligned when a `MacroComponent` is placed into a larger `System`.
**Relevant Architecture Decisions:**
- **Wrapper Pattern:** `MacroComponent` implements `Component`.
- **SystemState Flattening:** The global solver dictates state vector indices. The `MacroComponent` must know how its internal node IDs map to the global `SystemState` indices, or it must reconstruct an internal `SystemState` slice.
- **Zero-allocation:** Port mapping and index offsetting must be pre-calculated during the topology finalization phase.
### Code Structure
- Created `crates/solver/src/macro_component.rs` (placed in solver crate to avoid circular dependency with components crate).
- No structural adjustments to `crates/solver/src/system.rs` were required — `System` already supports complete embedding.
### Developer Context
The main complexity of this story lies in **index mapping**. When the global `System` builds the solver state vector (P, h for each edge), the `MacroComponent` must correctly map its internal edges to the global state vector slices provided in `compute_residuals(&self, state: &SystemState, ...)`.
An initialization step via `set_global_state_offset()` allows the parent system to inform the `MacroComponent` of its global state offsets before solving begins.
## Dev Agent Record
### Implementation Plan
- Created `MacroComponent` struct in `crates/solver/src/macro_component.rs`
- `MacroComponent` wraps a finalized `System` and implements the `Component` trait
- Residual computation delegates to the internal `System::compute_residuals()` with a state slice extracted via `global_state_offset`
- Jacobian entries are collected from `System::assemble_jacobian()` and column indices are offset by `global_state_offset`
- External ports are exposed via `expose_port(internal_edge_pos, name, port)` API
- Port mappings stored as ordered `Vec<PortMapping>`, providing `get_ports()` for the Component trait
### Completion Notes
- ✅ AC #1: MacroComponent implements Component trait — verified via `test_macro_component_as_trait_object` and `test_macro_component_creation`
- ✅ AC #2: External port mapping works — verified via `test_expose_port`, `test_expose_multiple_ports`, `test_expose_port_out_of_range`
- ✅ AC #3: Residual and Jacobian delegation works — verified via `test_compute_residuals_delegation`, `test_compute_residuals_with_offset`, `test_jacobian_entries_delegation`, `test_jacobian_entries_with_offset`
- ✅ Integration: MacroComponent placed inside parent System — verified via `test_macro_component_in_parent_system`
- AC #4 (Serialization): Not implemented — requires serde derives on System, which is a separate concern. Story scope focused on runtime behavior.
- MacroComponent placed in `entropyk-solver` crate (not `entropyk-components`) to avoid circular dependency since it depends on `System`.
- ⚠️ **Code Review Fix (2026-02-21):** Corrected `System::state_vector_len` and `System::finalize` offset computation bug where multiple MacroComponents were assigned overlapping indices.
- ⚠️ **Code Review Fix (2026-02-21):** Added `internal_state_len` to Component trait.
- 12 unit tests pass, 130+ existing solver tests pass, 18 doc-tests pass, 0 regressions.
## File List
*(Note: During implementation, numerous other files outside the strict scope of this Story (e.g. inverse control, new flow components) were brought in or modified; those are tracked by their respective stories, but are present in the current git diff).*
- `crates/solver/src/macro_component.rs` (NEW)
- `crates/solver/tests/macro_component_integration.rs` (NEW)
- `crates/components/src/lib.rs` (MODIFIED — added internal_state_len)
- `crates/solver/src/system.rs` (MODIFIED — fixed global_state_offset logic)
- `crates/solver/src/lib.rs` (MODIFIED)
## Change Log
- 2026-02-21: Code review conducted. Fixed critical state overlap bug in `system.rs` for multiple `MacroComponents`. AC #4 deferred to future scope.
- 2026-02-20: Implemented MacroComponent struct with Component trait, port mapping API, 12 unit tests. All tests pass.