Skip to content

Commit cb42a2b

Browse files
Phase 6.1 complete: added PT1/PT2 built-in process models with strict ModelSignature metadata, introduced minimal time units, expanded unit and analytical test coverage, and updated examples and documentation.
1 parent 9c8a11a commit cb42a2b

26 files changed

Lines changed: 1364 additions & 5 deletions

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ set(FLUXGRAPH_SOURCES
4646
src/model/thermal_integration.cpp
4747
src/model/thermal_mass.cpp
4848
src/model/thermal_rc2.cpp
49+
src/model/first_order_process.cpp
50+
src/model/second_order_process.cpp
4951
src/graph/compiler.cpp
5052
src/engine.cpp
5153
)

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ int main() {
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
129129
- [`05_thermal_rc2`](./examples/05_thermal_rc2/) - Two-node thermal RC model example
130+
- [`06_first_order_process`](./examples/06_first_order_process/) - First-order process primitive (PT1)
131+
- [`07_second_order_process`](./examples/07_second_order_process/) - Second-order process primitive (PT2)
130132

131133
## Project Structure
132134

docs/api-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ spec.models.push_back(model);
249249

250250
- `thermal_mass` - Heat transfer simulation
251251
- `thermal_rc2` - Two-node thermal RC network (coupled temperatures)
252+
- `first_order_process` - First-order process primitive (PT1)
253+
- `second_order_process` - Second-order process primitive (PT2)
252254

253255
#### RuleSpec
254256

docs/numerical-methods.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@ 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-
## Thermal Model Integration Methods
17+
## ODE Model Integration Methods
1818

19-
Both `thermal_mass` and `thermal_rc2` support:
19+
The following built-in models support `integration_method`:
20+
21+
1. `thermal_mass`
22+
2. `thermal_rc2`
23+
3. `first_order_process`
24+
4. `second_order_process`
25+
26+
Supported methods:
2027

2128
1. `forward_euler` (default)
2229
2. `rk4` (classic fourth-order Runge-Kutta)
@@ -50,6 +57,14 @@ the negative real axis). The stability limits use:
5057

5158
FluxGraph enforces these via `IModel::compute_stability_limit()`.
5259

60+
For the process primitives:
61+
62+
1. `first_order_process`: equivalent to `lambda = -1/tau_s`, so the same
63+
negative-real-axis limits apply.
64+
2. `second_order_process`: eigenvalues may be complex; stability is evaluated
65+
using the selected method's stability function `R(z)` along `z = lambda*dt`,
66+
requiring `|R(lambda*dt)| <= 1` for each eigenvalue.
67+
5368
## Validation Expectations
5469

5570
Validation runs must report:

docs/schema-json.md

Lines changed: 67 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 (built-in: `"thermal_mass"`, `"thermal_rc2"`)
61+
- `type` (string, required) - Model type (built-in: `"thermal_mass"`, `"thermal_rc2"`, `"first_order_process"`, `"second_order_process"`)
6262
- `params` (object, required) - Model-specific parameters
6363

6464
### ThermalMass Model
@@ -138,6 +138,72 @@ Two-node thermal RC network with ambient coupling and inter-node conductance.
138138
}
139139
```
140140

141+
### FirstOrderProcess Model
142+
143+
First-order process primitive: `dy/dt = (gain * u - y) / tau`.
144+
145+
**Parameters:**
146+
| Parameter | Type | Units | Description |
147+
|-----------|------|-------|-------------|
148+
| `output_signal` | string | dimensionless | Output signal path |
149+
| `input_signal` | string | dimensionless | Input signal path |
150+
| `gain` | number | dimensionless | Static gain (finite) |
151+
| `tau_s` | number | s | Time constant (must be > 0) |
152+
| `initial_output` | number | dimensionless | Initial output value |
153+
| `integration_method` | string | - | Optional: `"forward_euler"` (default) or `"rk4"` |
154+
155+
**Example:**
156+
157+
```json
158+
{
159+
"id": "pt1",
160+
"type": "first_order_process",
161+
"params": {
162+
"output_signal": "pt1.y",
163+
"input_signal": "pt1.u",
164+
"gain": 2.0,
165+
"tau_s": 1.0,
166+
"initial_output": 0.0
167+
}
168+
}
169+
```
170+
171+
### SecondOrderProcess Model
172+
173+
Second-order process primitive:
174+
`y'' + 2*zeta*omega_n*y' + omega_n^2*y = omega_n^2 * gain * u`
175+
176+
**Parameters:**
177+
| Parameter | Type | Units | Description |
178+
|-----------|------|-------|-------------|
179+
| `output_signal` | string | dimensionless | Output signal path |
180+
| `input_signal` | string | dimensionless | Input signal path |
181+
| `gain` | number | dimensionless | Static gain (finite) |
182+
| `zeta` | number | dimensionless | Damping ratio (must be >= 0) |
183+
| `omega_n_rad_s` | number | 1/s | Natural frequency (must be > 0) |
184+
| `initial_output` | number | dimensionless | Initial output value |
185+
| `initial_output_rate` | number | 1/s | Initial output rate |
186+
| `integration_method` | string | - | Optional: `"forward_euler"` (default) or `"rk4"` |
187+
188+
**Example:**
189+
190+
```json
191+
{
192+
"id": "pt2",
193+
"type": "second_order_process",
194+
"params": {
195+
"output_signal": "pt2.y",
196+
"input_signal": "pt2.u",
197+
"gain": 2.0,
198+
"zeta": 0.7,
199+
"omega_n_rad_s": 4.0,
200+
"initial_output": 0.0,
201+
"initial_output_rate": 0.0,
202+
"integration_method": "rk4"
203+
}
204+
}
205+
```
206+
141207
## Edges
142208

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

docs/schema-yaml.md

Lines changed: 63 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, thermal_rc2)
61+
type: model_type # Model type (thermal_mass, thermal_rc2, first_order_process, second_order_process)
6262
params: # Model-specific parameters
6363
param_name: value
6464
```
@@ -136,6 +136,68 @@ models:
136136
integration_method: rk4
137137
```
138138

139+
### FirstOrderProcess Model
140+
141+
First-order process primitive: `dy/dt = (gain * u - y) / tau`.
142+
143+
**Parameters:**
144+
| Parameter | Type | Units | Description |
145+
|-----------|------|-------|-------------|
146+
| `output_signal` | string | dimensionless | Output signal path |
147+
| `input_signal` | string | dimensionless | Input signal path |
148+
| `gain` | number | dimensionless | Static gain (finite) |
149+
| `tau_s` | number | s | Time constant (must be > 0) |
150+
| `initial_output` | number | dimensionless | Initial output value |
151+
| `integration_method` | string | - | Optional: `forward_euler` (default) or `rk4` |
152+
153+
**Example:**
154+
155+
```yaml
156+
models:
157+
- id: pt1
158+
type: first_order_process
159+
params:
160+
output_signal: pt1.y
161+
input_signal: pt1.u
162+
gain: 2.0
163+
tau_s: 1.0
164+
initial_output: 0.0
165+
```
166+
167+
### SecondOrderProcess Model
168+
169+
Second-order process primitive:
170+
`y'' + 2*zeta*omega_n*y' + omega_n^2*y = omega_n^2 * gain * u`
171+
172+
**Parameters:**
173+
| Parameter | Type | Units | Description |
174+
|-----------|------|-------|-------------|
175+
| `output_signal` | string | dimensionless | Output signal path |
176+
| `input_signal` | string | dimensionless | Input signal path |
177+
| `gain` | number | dimensionless | Static gain (finite) |
178+
| `zeta` | number | dimensionless | Damping ratio (must be >= 0) |
179+
| `omega_n_rad_s` | number | 1/s | Natural frequency (must be > 0) |
180+
| `initial_output` | number | dimensionless | Initial output value |
181+
| `initial_output_rate` | number | 1/s | Initial output rate |
182+
| `integration_method` | string | - | Optional: `forward_euler` (default) or `rk4` |
183+
184+
**Example:**
185+
186+
```yaml
187+
models:
188+
- id: pt2
189+
type: second_order_process
190+
params:
191+
output_signal: pt2.y
192+
input_signal: pt2.u
193+
gain: 2.0
194+
zeta: 0.7
195+
omega_n_rad_s: 4.0
196+
initial_output: 0.0
197+
initial_output_rate: 0.0
198+
integration_method: rk4
199+
```
200+
139201
## Edges
140202

141203
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_first_order_process main.cpp)
2+
target_link_libraries(example_first_order_process PRIVATE fluxgraph)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
spec.signals.push_back({"pt1.u", "dimensionless"});
15+
spec.signals.push_back({"pt1.y", "dimensionless"});
16+
17+
fluxgraph::ModelSpec model;
18+
model.id = "pt1";
19+
model.type = "first_order_process";
20+
model.params["output_signal"] = std::string("pt1.y");
21+
model.params["input_signal"] = std::string("pt1.u");
22+
model.params["gain"] = 2.0;
23+
model.params["tau_s"] = 1.0;
24+
model.params["initial_output"] = 0.0;
25+
model.params["integration_method"] = std::string("rk4"); // optional
26+
spec.models.push_back(model);
27+
28+
fluxgraph::GraphCompiler compiler;
29+
auto program = compiler.compile(spec, sig_ns, func_ns);
30+
31+
fluxgraph::Engine engine;
32+
engine.load(std::move(program));
33+
34+
const auto u_sig = sig_ns.resolve("pt1.u");
35+
const auto y_sig = sig_ns.resolve("pt1.y");
36+
37+
std::cout << "First-Order Process Simulation (PT1)\n";
38+
std::cout << "===================================\n";
39+
std::cout << std::fixed << std::setprecision(3);
40+
41+
constexpr double dt = 0.05;
42+
store.write(u_sig, 0.0, "dimensionless");
43+
44+
for (double t = 0.0; t <= 5.0; t += dt) {
45+
const double u = (t >= 1.0) ? 1.0 : 0.0;
46+
store.write(u_sig, u, "dimensionless");
47+
engine.tick(dt, store);
48+
49+
const double y = store.read_value(y_sig);
50+
std::cout << "t=" << std::setw(5) << t << "s u=" << u
51+
<< " y=" << std::setw(8) << y << "\n";
52+
}
53+
54+
return 0;
55+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
add_executable(example_second_order_process main.cpp)
2+
target_link_libraries(example_second_order_process PRIVATE fluxgraph)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
spec.signals.push_back({"pt2.u", "dimensionless"});
15+
spec.signals.push_back({"pt2.y", "dimensionless"});
16+
17+
fluxgraph::ModelSpec model;
18+
model.id = "pt2";
19+
model.type = "second_order_process";
20+
model.params["output_signal"] = std::string("pt2.y");
21+
model.params["input_signal"] = std::string("pt2.u");
22+
model.params["gain"] = 2.0;
23+
model.params["zeta"] = 0.7;
24+
model.params["omega_n_rad_s"] = 4.0;
25+
model.params["initial_output"] = 0.0;
26+
model.params["initial_output_rate"] = 0.0;
27+
model.params["integration_method"] = std::string("rk4"); // optional
28+
spec.models.push_back(model);
29+
30+
fluxgraph::GraphCompiler compiler;
31+
auto program = compiler.compile(spec, sig_ns, func_ns);
32+
33+
fluxgraph::Engine engine;
34+
engine.load(std::move(program));
35+
36+
const auto u_sig = sig_ns.resolve("pt2.u");
37+
const auto y_sig = sig_ns.resolve("pt2.y");
38+
39+
std::cout << "Second-Order Process Simulation (PT2)\n";
40+
std::cout << "====================================\n";
41+
std::cout << std::fixed << std::setprecision(3);
42+
43+
constexpr double dt = 0.01;
44+
store.write(u_sig, 0.0, "dimensionless");
45+
46+
for (double t = 0.0; t <= 5.0; t += dt) {
47+
const double u = (t >= 1.0) ? 1.0 : 0.0;
48+
store.write(u_sig, u, "dimensionless");
49+
engine.tick(dt, store);
50+
51+
const double y = store.read_value(y_sig);
52+
if (static_cast<int>(t / dt) % 10 == 0) {
53+
std::cout << "t=" << std::setw(5) << t << "s u=" << u
54+
<< " y=" << std::setw(8) << y << "\n";
55+
}
56+
}
57+
58+
return 0;
59+
}

0 commit comments

Comments
 (0)