Skip to content

Commit 9c8a11a

Browse files
Add thermal_rc2 two-node thermal model with shared thermal integration utilities, docs, tests, and example.
1 parent 1b08d8a commit 9c8a11a

20 files changed

Lines changed: 882 additions & 45 deletions

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ set(FLUXGRAPH_SOURCES
4343
src/core/signal_store.cpp
4444
src/core/namespace.cpp
4545
src/core/units.cpp
46+
src/model/thermal_integration.cpp
4647
src/model/thermal_mass.cpp
48+
src/model/thermal_rc2.cpp
4749
src/graph/compiler.cpp
4850
src/engine.cpp
4951
)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ int main() {
126126
- [`02_thermal_mass`](./examples/02_thermal_mass/) - Physics simulation with models
127127
- [`03_json_graph`](./examples/03_json_graph/) - Load graph from JSON file
128128
- [`04_yaml_graph`](./examples/04_yaml_graph/) - Load graph from YAML file
129+
- [`05_thermal_rc2`](./examples/05_thermal_rc2/) - Two-node thermal RC model example
129130

130131
## Project Structure
131132

docs/api-reference.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,8 @@ model.type = "thermal_mass";
238238
model.params["temp_signal"] = std::string("chamber.temp");
239239
model.params["power_signal"] = std::string("chamber.power");
240240
model.params["ambient_signal"] = std::string("ambient.temp");
241-
model.params["thermal_mass"] = 1000.0; // J/degC
242-
model.params["heat_transfer_coeff"] = 10.0; // W/degC
241+
model.params["thermal_mass"] = 1000.0; // J/K
242+
model.params["heat_transfer_coeff"] = 10.0; // W/K
243243
model.params["initial_temp"] = 25.0; // degC
244244
model.params["integration_method"] = std::string("forward_euler"); // optional: "forward_euler" (default) or "rk4"
245245
spec.models.push_back(model);
@@ -248,6 +248,7 @@ spec.models.push_back(model);
248248
**Available model types:**
249249

250250
- `thermal_mass` - Heat transfer simulation
251+
- `thermal_rc2` - Two-node thermal RC network (coupled temperatures)
251252

252253
#### RuleSpec
253254

docs/numerical-methods.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ Validation protocol details are documented in `docs/validation-methodology.md`.
1414
3. Methods are selected at compile time and remain fixed at runtime.
1515
4. Runtime behavior must be deterministic for identical inputs and `dt`.
1616

17-
## ThermalMassModel Integration Methods
17+
## Thermal Model Integration Methods
1818

19-
`thermal_mass` supports:
19+
Both `thermal_mass` and `thermal_rc2` support:
2020

2121
1. `forward_euler` (default)
2222
2. `rk4` (classic fourth-order Runge-Kutta)
@@ -33,14 +33,23 @@ Unknown method names are compile-time errors.
3333

3434
## Stability Limits
3535

36-
For the linear thermal model `dT/dt = (P - h*(T - T_amb)) / C`, with
37-
`k = h/C`:
36+
For the linear thermal mass model `dT/dt = (P - h*(T - T_amb)) / C`, with
37+
`k = h/C`, the stability limits are:
3838

3939
1. `forward_euler`: `dt < 2/k = 2*C/h`
4040
2. `rk4`: `dt < 2.785293563/k = 2.785293563*C/h`
4141

4242
If `h <= 0`, the model is treated as unconditionally stable for this criterion.
4343

44+
For the two-node thermal RC model, the system is linear: `dT/dt = A*T + c`.
45+
Let `lambda_min` be the most negative eigenvalue of `A` (largest magnitude on
46+
the negative real axis). The stability limits use:
47+
48+
1. `forward_euler`: `dt < 2/|lambda_min|`
49+
2. `rk4`: `dt < 2.785293563/|lambda_min|`
50+
51+
FluxGraph enforces these via `IModel::compute_stability_limit()`.
52+
4453
## Validation Expectations
4554

4655
Validation runs must report:

docs/schema-json.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Models represent physical systems with internal state and differential equations
5858
**Fields:**
5959

6060
- `id` (string, required) - Unique model identifier
61-
- `type` (string, required) - Model type (`"thermal_mass"` only in v1.0)
61+
- `type` (string, required) - Model type (built-in: `"thermal_mass"`, `"thermal_rc2"`)
6262
- `params` (object, required) - Model-specific parameters
6363

6464
### ThermalMass Model
@@ -74,6 +74,7 @@ Lumped thermal capacitance with heat transfer: `C * dT/dt = P - h * (T - T_ambie
7474
| `thermal_mass` | number | J/K | Heat capacity (must be > 0) |
7575
| `heat_transfer_coeff` | number | W/K | Heat transfer coefficient (must be > 0) |
7676
| `initial_temp` | number | degC | Initial temperature |
77+
| `integration_method` | string | - | Optional: `"forward_euler"` (default) or `"rk4"` |
7778

7879
**Example:**
7980

@@ -94,6 +95,49 @@ Lumped thermal capacitance with heat transfer: `C * dT/dt = P - h * (T - T_ambie
9495

9596
**Stability:** Forward Euler integration requires `dt < 2 * thermal_mass / heat_transfer_coeff`
9697

98+
### ThermalRc2 Model
99+
100+
Two-node thermal RC network with ambient coupling and inter-node conductance.
101+
102+
**Parameters:**
103+
| Parameter | Type | Units | Description |
104+
|-----------|------|-------|-------------|
105+
| `temp_signal_a` | string | - | Output signal path for node A temperature |
106+
| `temp_signal_b` | string | - | Output signal path for node B temperature |
107+
| `power_signal` | string | W | Input signal path for heating power (applied to node A) |
108+
| `ambient_signal` | string | degC | Input signal path for ambient temperature |
109+
| `thermal_mass_a` | number | J/K | Heat capacity of node A (must be > 0) |
110+
| `thermal_mass_b` | number | J/K | Heat capacity of node B (must be > 0) |
111+
| `heat_transfer_coeff_a` | number | W/K | Ambient coupling of node A (must be > 0) |
112+
| `heat_transfer_coeff_b` | number | W/K | Ambient coupling of node B (must be > 0) |
113+
| `coupling_coeff` | number | W/K | Conductance between nodes (must be >= 0) |
114+
| `initial_temp_a` | number | degC | Initial temperature of node A |
115+
| `initial_temp_b` | number | degC | Initial temperature of node B |
116+
| `integration_method` | string | - | Optional: `"forward_euler"` (default) or `"rk4"` |
117+
118+
**Example:**
119+
120+
```json
121+
{
122+
"id": "chamber_rc",
123+
"type": "thermal_rc2",
124+
"params": {
125+
"temp_signal_a": "chamber.shell_temp",
126+
"temp_signal_b": "chamber.core_temp",
127+
"power_signal": "chamber.heater_power",
128+
"ambient_signal": "ambient.temp",
129+
"thermal_mass_a": 1000.0,
130+
"thermal_mass_b": 2000.0,
131+
"heat_transfer_coeff_a": 10.0,
132+
"heat_transfer_coeff_b": 8.0,
133+
"coupling_coeff": 6.0,
134+
"initial_temp_a": 25.0,
135+
"initial_temp_b": 25.0,
136+
"integration_method": "rk4"
137+
}
138+
}
139+
```
140+
97141
## Edges
98142

99143
Edges connect signals through transforms, defining the dataflow graph.

docs/schema-yaml.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Models represent physical systems with internal state and differential equations
5858

5959
```yaml
6060
- id: unique_identifier # Unique model identifier
61-
type: model_type # Model type (thermal_mass in v1.0)
61+
type: model_type # Model type (thermal_mass, thermal_rc2)
6262
params: # Model-specific parameters
6363
param_name: value
6464
```
@@ -76,6 +76,7 @@ Lumped thermal capacitance with heat transfer: `C * dT/dt = P - h * (T - T_ambie
7676
| `thermal_mass` | number | J/K | Heat capacity (must be > 0) |
7777
| `heat_transfer_coeff` | number | W/K | Heat transfer coefficient (must be > 0) |
7878
| `initial_temp` | number | degC | Initial temperature |
79+
| `integration_method` | string | - | Optional: `forward_euler` (default) or `rk4` |
7980

8081
**Example:**
8182

@@ -94,6 +95,47 @@ models:
9495

9596
**Stability:** Forward Euler integration requires `dt < 2 * thermal_mass / heat_transfer_coeff`
9697

98+
### ThermalRc2 Model
99+
100+
Two-node thermal RC network with ambient coupling and inter-node conductance.
101+
102+
**Parameters:**
103+
| Parameter | Type | Units | Description |
104+
|-----------|------|-------|-------------|
105+
| `temp_signal_a` | string | - | Output signal path for node A temperature |
106+
| `temp_signal_b` | string | - | Output signal path for node B temperature |
107+
| `power_signal` | string | W | Input signal path for heating power (applied to node A) |
108+
| `ambient_signal` | string | degC | Input signal path for ambient temperature |
109+
| `thermal_mass_a` | number | J/K | Heat capacity of node A (must be > 0) |
110+
| `thermal_mass_b` | number | J/K | Heat capacity of node B (must be > 0) |
111+
| `heat_transfer_coeff_a` | number | W/K | Ambient coupling of node A (must be > 0) |
112+
| `heat_transfer_coeff_b` | number | W/K | Ambient coupling of node B (must be > 0) |
113+
| `coupling_coeff` | number | W/K | Conductance between nodes (must be >= 0) |
114+
| `initial_temp_a` | number | degC | Initial temperature of node A |
115+
| `initial_temp_b` | number | degC | Initial temperature of node B |
116+
| `integration_method` | string | - | Optional: `forward_euler` (default) or `rk4` |
117+
118+
**Example:**
119+
120+
```yaml
121+
models:
122+
- id: chamber_rc
123+
type: thermal_rc2
124+
params:
125+
temp_signal_a: chamber.shell_temp
126+
temp_signal_b: chamber.core_temp
127+
power_signal: chamber.heater_power
128+
ambient_signal: ambient.temp
129+
thermal_mass_a: 1000.0
130+
thermal_mass_b: 2000.0
131+
heat_transfer_coeff_a: 10.0
132+
heat_transfer_coeff_b: 8.0
133+
coupling_coeff: 6.0
134+
initial_temp_a: 25.0
135+
initial_temp_b: 25.0
136+
integration_method: rk4
137+
```
138+
97139
## Edges
98140

99141
Edges connect signals through transforms, defining the dataflow graph.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
add_executable(example_thermal_rc2 main.cpp)
2+
target_link_libraries(example_thermal_rc2 PRIVATE fluxgraph)

examples/05_thermal_rc2/main.cpp

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include <fluxgraph/core/namespace.hpp>
2+
#include <fluxgraph/core/signal_store.hpp>
3+
#include <fluxgraph/engine.hpp>
4+
#include <fluxgraph/graph/compiler.hpp>
5+
#include <iomanip>
6+
#include <iostream>
7+
8+
int main() {
9+
fluxgraph::SignalNamespace sig_ns;
10+
fluxgraph::FunctionNamespace func_ns;
11+
fluxgraph::SignalStore store;
12+
13+
fluxgraph::GraphSpec spec;
14+
15+
fluxgraph::ModelSpec model;
16+
model.id = "chamber_rc";
17+
model.type = "thermal_rc2";
18+
model.params["temp_signal_a"] = std::string("chamber.shell_temp");
19+
model.params["temp_signal_b"] = std::string("chamber.core_temp");
20+
model.params["power_signal"] = std::string("chamber.heater_power");
21+
model.params["ambient_signal"] = std::string("ambient.temp");
22+
model.params["thermal_mass_a"] = 1000.0; // J/K
23+
model.params["thermal_mass_b"] = 2000.0; // J/K
24+
model.params["heat_transfer_coeff_a"] = 10.0; // W/K
25+
model.params["heat_transfer_coeff_b"] = 8.0; // W/K
26+
model.params["coupling_coeff"] = 6.0; // W/K
27+
model.params["initial_temp_a"] = 25.0; // degC
28+
model.params["initial_temp_b"] = 25.0; // degC
29+
model.params["integration_method"] = std::string("rk4"); // optional
30+
spec.models.push_back(model);
31+
32+
fluxgraph::GraphCompiler compiler;
33+
auto program = compiler.compile(spec, sig_ns, func_ns);
34+
35+
fluxgraph::Engine engine;
36+
engine.load(std::move(program));
37+
38+
const auto heater_sig = sig_ns.resolve("chamber.heater_power");
39+
const auto ambient_sig = sig_ns.resolve("ambient.temp");
40+
const auto shell_temp_sig = sig_ns.resolve("chamber.shell_temp");
41+
const auto core_temp_sig = sig_ns.resolve("chamber.core_temp");
42+
43+
store.write(ambient_sig, 20.0, "degC");
44+
45+
std::cout << "Thermal RC2 Simulation\n";
46+
std::cout << "======================\n";
47+
std::cout << std::fixed << std::setprecision(2);
48+
49+
for (double t = 0.0; t <= 10.0; t += 0.5) {
50+
const double heater_power = (t < 5.0) ? 500.0 : 0.0;
51+
store.write(heater_sig, heater_power, "W");
52+
53+
engine.tick(0.5, store);
54+
55+
const double shell_temp = store.read_value(shell_temp_sig);
56+
const double core_temp = store.read_value(core_temp_sig);
57+
58+
std::cout << "t=" << std::setw(5) << t << "s "
59+
<< "Heater=" << std::setw(6) << heater_power << "W "
60+
<< "Shell=" << std::setw(6) << shell_temp << " degC "
61+
<< "Core=" << std::setw(6) << core_temp << " degC\n";
62+
}
63+
64+
return 0;
65+
}

examples/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
# Example 2: Thermal mass - Realistic physics simulation with models
55
# Example 3: JSON loader - Load graph from JSON file (requires -DFLUXGRAPH_JSON_ENABLED=ON)
66
# Example 4: YAML loader - Load graph from YAML file (requires -DFLUXGRAPH_YAML_ENABLED=ON)
7+
# Example 5: Thermal RC2 - Coupled two-node thermal model
78

89
add_subdirectory(01_basic_transform)
910
add_subdirectory(02_thermal_mass)
1011
add_subdirectory(03_json_graph)
1112
add_subdirectory(04_yaml_graph)
13+
add_subdirectory(05_thermal_rc2)

examples/README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,22 @@ cmake --build build-yaml --config Debug
165165
- Optional dependency (core library still zero-dep)
166166
- Can enable both JSON and YAML simultaneously
167167

168+
## Example 5: Two-Node Thermal RC Simulation
169+
170+
**Location:** `05_thermal_rc2/`
171+
172+
Demonstrates a coupled two-temperature physics model:
173+
174+
- `thermal_rc2` - Two-node thermal RC network (shell/core style)
175+
- Multiple model outputs (`temp_signal_a`, `temp_signal_b`)
176+
- Ambient coupling + inter-node conductance
177+
178+
**Run:**
179+
180+
```bash
181+
./build/examples/05_thermal_rc2/Debug/example_thermal_rc2.exe
182+
```
183+
168184
## When to Use Each Approach
169185

170186
### Manual GraphSpec (Examples 1 & 2)
@@ -255,7 +271,7 @@ double result = store.read_value(output_sig);
255271
256272
**"Unknown model type" error:**
257273
258-
- Check ModelSpec `type` field matches implemented model ("thermal_mass")
274+
- Check ModelSpec `type` field matches implemented model ("thermal_mass" or "thermal_rc2")
259275
- Ensure all required params are present
260276
261277
**Signals read as 0.0:**
@@ -267,7 +283,7 @@ double result = store.read_value(output_sig);
267283
**Unexpected NaN/Inf values:**
268284
269285
- Check model stability limits (dt too large)
270-
- Verify thermal_mass and heat_transfer_coeff > 0
286+
- Verify model parameters satisfy constraints (for example: thermal_mass > 0, heat_transfer_coeff > 0)
271287
- Ensure ambient temperature is initialized
272288
273289
**Compile errors:**

0 commit comments

Comments
 (0)