Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ Major release, not backwards compatible except for the AgentLib-MPC integration
https://rwth-ebc.github.io/physXAI/main/docs/index.html.

### Changed
- `PINNModel(physics_targets={'pinn': w})`: physics targets are loss columns, no model outputs, so a PINN has one
output and is exportable to AgentLib-MPC. The loss `mse(ŷ, y)/s + Σ w_i·mse(ŷ, p_i)/s` is normalized by the
variance of the training targets and no longer averaged over the terms; early stopping monitors the validation
data loss (`early_stopping_on`); history, metrics (`td.metrics.physics`) and report hold every term.
- `loss_columns` in the preprocessing: columns used only in the loss, added from `model.required_loss_columns` by
`prep.pipeline(path, model=model)`.
- `FeatureSelectionResult.save(run)` replaces `save_result`.
- Unknown arguments of the preprocessing raise a `TypeError` instead of being ignored.
- Python >= 3.11 is required (3.9 and 3.10 are end of life).
- CI uses the shared template of the institute (RWTH-EBC/ci_templates, like AgentLib and AgentLib-MPC): tests,
coverage, package build, pylint/ruff reports, documentation and GitHub Pages; additionally the unit tests on
Expand Down Expand Up @@ -113,6 +121,12 @@ Major release, not backwards compatible except for the AgentLib-MPC integration
- `PreprocessingData.from_config` restores the class from `__class_name__` and all settings.

### Fixed
- PINN: the arguments of the loss were swapped (`loss(y_pred, y_true)`), the RMSE metric accumulated over the data
and physics terms, adding a physics term halved the weight of the data, the output rescaling used the physics
columns, and a PINN could not be exported (two outputs).
- Single step data with `test_size=0`: the evaluation predicted on an empty test set.
- `px.Linear(bias=False)` with normalized inputs warns that the normalization acts as a bias; `nominal_range` has
to be ordered.
- Multi step training and test windows overlapped (data leakage).
- With `ignore_nan=True`, targets were taken across gaps in the data.
- `PreprocessingMultiStep` ignored `ignore_nan`; configs missed csv options, so `from_config` was not
Expand All @@ -135,6 +149,10 @@ Major release, not backwards compatible except for the AgentLib-MPC integration
- Feature selection: `input()` prompts, the preprocessing was changed in place, lags were parsed from strings.

### Removed
- Compatibility code of 0.x: `recursive_feature_elimination_pipeline` and `save_result`, the flat metrics
(`train_kpis`, `val_kpis`, `test_kpis`, `get_metrics`), `td.columns` / `td.output`, `pinn_weights` and
`multi_y_loss`, the arguments `init_layer`, `init_features`, `warmup_columns_*` and the `batch_size` of
`PreprocessingMultiStep`.
- `physXAI.preprocessing.constructed` (`Feature`, `FeatureConstruction`): lags and `Change(X)` are derived
natively, other quantities are expressions (`physXAI.expr`), preferably inside the model.
- `physXAI.models.modular.modular_expression` and the `Modular*` classes: replaced by `physXAI.expr`
Expand All @@ -143,7 +161,7 @@ Major release, not backwards compatible except for the AgentLib-MPC integration
`ModularExistingModel` -> `px.Pretrained`). The separate `modular_expression_config.json` is gone, the graph
is part of the model config.
- RNN initialization models (`init_layer`) and the warmup features of the preprocessing (`init_features`,
`warmup_columns_*`). Old configs without initialization model are still accepted.
`warmup_columns_*`).
- Multi step evaluation of single step models with `PreprocessingMultiStep` (`SingleStepModel.evaluate_multi`):
use `PreprocessingSingleStep(eval_horizon=...)`. `physXAI.evaluation.closed_loop` is replaced by the
closed loop of `RNNModel` and by `physXAI.evaluation.rollout`.
Expand Down
7 changes: 3 additions & 4 deletions docs/source/guide/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
After `model.pipeline(td)`:

- `td.metrics.by_output[output][split]`: MSE, RMSE, MAE, R², bias (mean of prediction − truth) and the
number of samples, for every output and split separately. The flat dicts `td.metrics.test_kpis` etc.
(`'RMSE Test'`) hold the mean over the outputs.
number of samples, for every output and split separately; `td.metrics.kpi('test', 'rmse')` returns one
value. Splits without data (`test_size=0`, `val_size=0`) are left out.
- `td.horizon_metrics`: RMSE, MAE, bias and standard deviation per step of the horizon (see
[Multi step evaluation](multi_step.md)), per predicted feature in `by_feature`.

Expand Down Expand Up @@ -99,7 +99,6 @@ See `executables/bestest_hydronic_heat_pump/TAir_compare.py`.
`FeatureSelection` removes inputs one by one (backward elimination) and keeps the best set:

```python
from physXAI.feature_selection.recursive_feature_elimination import save_result
from physXAI.feature_selection.selection import FeatureSelection

prep = PreprocessingSingleStep(inputs=candidates, output="Change(reaTZon_y)", eval_horizon=48)
Expand All @@ -108,7 +107,7 @@ selection = FeatureSelection(prep, LinearRegressionModel(), first_step_weight=0.
result = selection.run(path)
result.selected.inputs
result.table()
save_result(result, run) # feature_selection.json and .html
result.save(run) # feature_selection.json and .html
```

- The criterion is the step weighted RMSE of the recursive prediction over `eval_horizon` (optionally plus
Expand Down
31 changes: 30 additions & 1 deletion docs/source/guide/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,42 @@ Predict the next time step. Applied recursively in the MPC when they predict one
| `CMNNModel(monotonies, activation_split)` | constrained monotonic neural network |
| `RBFModel(n_neurons)` | radial basis function network |
| `LinANNModel()` | linear regression plus an RBF network on its residuals |
| `PINNModel(pinn_weights)` | physics informed loss on additional targets (`columns`) |
| `PINNModel(physics_targets)` | physics-informed loss on physics targets, see below |
| `ModularANN(architecture, solver='adam' or 'ols')` | any expression graph, see [Expressions](expressions.md) |
| `ConstrainedRegression(inputs, monotonies, convexities)` | constrained linear regression on expressions |

Keras models share the training options `epochs`, `learning_rate`, `batch_size`,
`early_stopping_epochs` and `random_seed`. Early stopping needs validation data.

## Physics-informed neural networks

A physics target is a data column computed from a physical model of the output. The network keeps one
output; the physics targets only enter the loss:

```python
v = px.Vars()
physics = v.u_hp * 10000 * (v.T_room + 15 - v.T_amb) / ((v.T_room + 15) * 0.55)
prep = PreprocessingSingleStep(inputs=[...], output='P_el', columns={'pinn': physics})
m = PINNModel(physics_targets={'pinn': 0.5})
td = prep.pipeline(path, model=m) # the model adds its physics targets to the loss columns
m.pipeline(td)
```

- Loss: `mse(ŷ, y)/s + Σ w_i · mse(ŷ, p_i)/s` with the characteristic scale `s = var(y_train)`
(`normalize_terms=True`). The data term is 1 − R² on the training data, the weight `w_i` the relative
importance of the physics. The terms are not averaged, adding a physics term does not change the weight of
the data.
- The weight encodes the trust in the physical model and is chosen by validation (e.g. the error over the
horizon, see [Comparing models](evaluation.md#comparing-models)). Adaptive weighting schemes (e.g. GradNorm,
learning rate annealing) target PDE residuals of very different magnitudes and are not used.
- Early stopping monitors the validation **data** loss (`early_stopping_on='data'`), so the selected model does
not depend on the physics weights; `'total'` monitors the weighted loss.
- History and report hold every term (`data_loss`, `<target>_loss`); `td.metrics.physics` the disagreement with
each physics target per split.
- Physics targets are taken at the predicted time, like the output. For `Change(T)` and a physical model of
the step T(t) → T(t+1) computed from the data at t, shift the column: `columns={'dT': step.lag(1)}`.
- The saved model has one output and is exported to AgentLib-MPC like any other model.

## Multi step models

`RNNModel(rnn_layer='RNN' | 'GRU' | 'LSTM', rnn_units)` predicts a trajectory, see
Expand Down
11 changes: 9 additions & 2 deletions docs/source/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ script signature and the saved files are the same.
| `RNNModel(init_layer=...)`, `PreprocessingMultiStep(init_features=...)` | warmup from zero states: `warmup_width` |
| `PreprocessingMultiStep(batch_size=...)` | `RNNModel(batch_size=...)` |
| single step model on `PreprocessingMultiStep` data (`evaluate_multi`) | `PreprocessingSingleStep(eval_horizon='24h')` |
| `recursive_feature_elimination_pipeline` with `input()` prompts | `FeatureSelection(...).run(path)` |
| `recursive_feature_elimination_pipeline`, `save_result` | `FeatureSelection(...).run(path)`, `result.save(run)` |
| `PINNModel(pinn_weights=[...])` with `output=['y', 'pinn']` | `PINNModel(physics_targets={'pinn': w})`, `output='y'`, `prep.pipeline(path, model=m)` |
| `td.metrics.test_kpis['RMSE Test']`, `get_metrics()` | `td.metrics.by_output[output]['test']['rmse']` or `td.metrics.kpi('test', 'rmse')` |
| `td.columns`, `td.output` | `td.inputs`, `td.outputs` |
| plots open in the browser | saved as html; `show_plot=True` opens them |
| random row split | block split without leakage (`split='random_rows'` for the old behaviour) |
| `np.random.seed(); random_state=np.random.randint(...)` | `random_state=None` (a seed is drawn and stored) |
Expand Down Expand Up @@ -110,7 +113,11 @@ return run.save_for_agentlib(output_name, prep, model, td)
- **Gaps:** data is put onto a regular time grid; targets are never taken across gaps.
- **Saving:** `pipeline` saves nothing without a `save_path`. Use `run.save`.
- **Plots:** not opened by default.
- **Metrics:** per output (`td.metrics.by_output`); the flat keys stay and hold the mean over the outputs.
- **Metrics:** per output (`td.metrics.by_output`); the flat keys of 0.x are removed.
- **PINN:** one model output (exportable); the loss is normalized by the variance of the training targets and no
longer averaged over the terms, so weights of 0.x runs are not comparable one to one.
- **Arguments of 0.x** (`init_layer`, `init_features`, `batch_size` of the preprocessing, `pinn_weights`) are not
accepted anymore.
- **RNNs:** the saved `.keras` file is the step model used by AgentLib-MPC; RNNs with initialization
models have to be retrained.
- **Old runs** can still be compared with new ones: `ModelComparison.load` reads training data files of 0.x.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from physXAI.feature_selection.recursive_feature_elimination import save_result
from physXAI.feature_selection.selection import FeatureSelection
from physXAI.io import Run
from physXAI.models.models import LinearRegressionModel
Expand Down Expand Up @@ -35,4 +34,4 @@
print('Selected inputs:', result.selected.inputs)

# Save the result (json) and its plot (html) into the run directory
save_result(result, run)
result.save(run)
33 changes: 17 additions & 16 deletions executables/bestest_hydronic_heat_pump/P_hp_pinn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
from physXAI.preprocessing.preprocessing import PreprocessingSingleStep

"""
Creates a physics-informed model (PINN) to predict the power of the heat pump using the Boptest data
Creates a physics-informed model (PINN) to predict the power of the heat pump using the Boptest data.

- The model has one output, the electrical power of the heat pump, and is used like any other model
(also in AgentLib-MPC).
- A simplified physical model of the heat pump is a physics target: the network is trained on the data
and, with a weight, on the agreement with the physical model. All terms of the loss are divided by the
variance of the training targets, the weight is the relative importance of the physics.
"""
# Directory of the run (stored_data/P_hp_pinn)
run = Run('stored_data', 'P_hp_pinn', overwrite=True)
Expand All @@ -14,16 +20,12 @@

# List of input features
inputs = ['oveHeaPumY_u', 'Func(logistic)', 'weaSta_reaWeaTDryBul_y', 'reaTZon_y']
# Output feature
output = 'reaPHeaPum_y'

"""
Output feature(s). In addition to the standard data-driven loss, the PINN includes a physical loss function,
which is computed from the data as an expression
"""
output = ['reaPHeaPum_y', 'pinn']

"""
The physical target 'pinn' is an expression of data columns, added to the data via `columns`.
It serves as a physical loss function for the PINN.
Physical model of the power: an expression of data columns, added to the data via `columns`.
Like the output, it is taken at the predicted time.
"""
v = px.Vars()
u_hp, u_hp_logistic = v.oveHeaPumY_u, v['Func(logistic)']
Expand All @@ -32,17 +34,16 @@

# Create Training data
prep = PreprocessingSingleStep(inputs=inputs, output=output, columns={'pinn': pinn})
# Process Training data
td = prep.pipeline(file_path)

"""
Create PINN model.
pinn_weights are used to balance the individual loss therms of the PINN, their length should be `num_outputs - 1`
This ´PINN implementation uses a CMNN as its base architecture
Create the PINN (a CMNN as base architecture). physics_targets maps the physics columns to their weight.
"""
m = PINNModel(pinn_weights=[1], epochs=50)
m = PINNModel(physics_targets={'pinn': 0.5}, epochs=50)

# Process Training data: the model adds its physics targets to the loss columns of the preprocessing
td = prep.pipeline(file_path, model=m)

# Training pipeline
# Training pipeline (early stopping on the validation error of the data)
model = m.pipeline(td)

# Save the configs of preprocessing and model, the metrics (training_data.json), the model and its report
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from physXAI.feature_selection.recursive_feature_elimination import save_result
from physXAI.feature_selection.selection import FeatureSelection
from physXAI.io import Run
from physXAI.models.models import LinearRegressionModel
Expand Down Expand Up @@ -39,4 +38,4 @@
print(result.table()[['n_inputs', 'score', 'score_std', 'single_step_rmse', 'plausible']].to_string())

# Save the result (json) and its plot (html) into the run directory
save_result(result, run)
result.save(run)
Loading
Loading