# 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.

