Metadata-Version: 2.5
Name: cppmodel
Version: 0.13.0
Summary: Write CppModel simulations in Python
Project-URL: Homepage, https://cppmodel.com
Author-email: Control Edge AB <support@cedge.se>
License-Expression: LicenseRef-CppModel
License-File: LICENSE
License-File: licenses/IXWebSocket-LICENSE.txt
License-File: licenses/cpp-httplib-LICENSE.txt
License-File: licenses/jwt-cpp-LICENSE.txt
License-File: licenses/nlohmann-json-LICENSE.txt
License-File: licenses/openssl-LICENSE.txt
License-File: licenses/picojson-LICENSE.txt
Keywords: cppmodel,embedded,plant model,simulation,testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# CppModel for Python

Write [CppModel](https://cppmodel.com) simulations in Python. Every run fetches the inputs and
parameters posted for it in your CppModel workspace and submits its execution there, just like a
C or C++ simulation built with the CppModel SDK.

```sh
pip install cppmodel
```

Wheels are available for Linux (x86_64 and aarch64, glibc 2.28+), Windows (x64 and ARM64) and
macOS 11+ (Apple silicon and Intel), for Python 3.9 and later. Each wheel contains the CppModel
library, so nothing else needs installing.

## A simulation

```python
import sys
import cppmodel


class Heater(cppmodel.Simulation):
    def init(self):
        self.temperature = 20.0

    def step(self, time_ms):
        setpoint = self.inputs.get("Setpoint", 22.0)        # input, with its fallback
        gain = self.parameters.get("HeaterGain", 0.05)      # parameter, with its fallback

        heater_on = self.temperature < setpoint             # the controller under test
        self.temperature += gain if heater_on else -0.01

        self.outputs["Temperature"] = self.temperature
        self.outputs["HeaterOn"] = heater_on
        self.outputs["CppModel.Step"] = time_ms
        self.outputs["CppModel.StepResult"] = self.temperature < setpoint + 1.0


with Heater("Example - Heater (Python)", total_time_ms=60_000, step_ms=100) as simulation:
    sys.exit(simulation.run())
```

`run()` fetches the pending inputs and parameters, calls `init()`, then `step(time_ms)` for every
step from 0 to `total_time_ms`, then `finalize()`, and submits the execution. It returns 0 when every
step passed, otherwise the `CppModel.Step` of the first step whose `CppModel.StepResult` was 0.

An exception raised in `init()`, `step()` or `finalize()` (Ctrl-C included) aborts the run, submits
nothing and is re-raised by `run()`.

Instead of subclassing, you can pass functions that take the simulation as their first argument:

```python
def ramp(sim, time_ms):
    sim.outputs["Ramp"] = time_ms

sim = cppmodel.Simulation("Ramp", total_time_ms=1000, step_ms=100, step=ramp)
sim.run()
```

## Inputs, outputs and parameters

| | |
|---|---|
| `sim.inputs.get(name, fallback)` | The input's value at the current step, or `fallback` (recorded as its value) |
| `sim.inputs[name]` | Same, with fallback 0 |
| `sim.inputs.load([{"label", "x", "y"}, ...])` | Loads input series locally (x in ms) |
| `sim.outputs[name] = value` | Sets an output; it holds until set again and is recorded every step. Values are numbers; bools become 1/0 |
| `sim.parameters.get(name, fallback)` | The parameter, storing `fallback` when it isn't set; a fetched value replaces the fallback |
| `sim.parameters[name] = value` | Sets a parameter that fetched values never replace |
| `sim.parameters[name]` | The parameter, or `KeyError` |
| `sim.results()` | The execution as submitted: `{"inputs", "results", "parameters", ...}` |

Inputs and outputs are numbers. Parameters keep their JSON type: numbers, bools, strings, lists,
dicts and `None`. All three support `in`, `len()`, iteration and `keys()`.

## Verification rules and requirements

Rules check how a run behaves over time. Each one is an object you create once, after
`super().__init__()`. The CppModel library tracks it:

- **Failing steps:** a step at which a rule is violated, or has certainly failed and still is, gets
  `CppModel.StepResult` 0, so the run fails.
- **Per-rule output:** each rule records the output `CppModel.Verify.<name>`: 1 until it first
  fails, then 0.
- **Outcome:** when the run ends, each rule's outcome is printed and stored under the rule's name
  in the execution's `verification` field (`sim.results()["verification"]`).

```python
from cppmodel import verify


class Zone(cppmodel.Simulation):
    def __init__(self):
        super().__init__("Zone", total_time_ms=3_600_000, step_ms=1000)
        self.valve_range = verify.Always(self, "valve range")
        self.smoke = verify.Response(self, "smoke stops fan", deadline_s=2, hold_s=60)
        self.step_response = verify.Settle(self, "setpoint step", band=0.5, settle_limit_s=1800, stay_s=600)
        self.modes = verify.Mode(self, "modes").allow(OFF, STARTUP).allow(STARTUP, RUN).dwell(STARTUP, max_s=30)
        verify.Rate(self, "temperature rate", "Zone.T", max_rate=0.01)  # reads the output by itself
        self.verify_requirement("ZONE-SAF-001")  # this run verifies the requirement

    def step(self, time_ms):
        ...
        self.valve_range(verify.between(valve, 0, 100))
        self.smoke(smoke, not fan and valve == 0)
        self.step_response(temperature, setpoint)
        self.modes(mode)
        if smoke:
            self.verify_requirement("ZONE-SAF-004")  # verified at these steps
```

**Step rules** are called in `step()`:

- `Always(sim, name, debounce_s=0)`;
- `Response(sim, name, deadline_s, hold_s=0)`;
- `Settle(sim, name, band, settle_limit_s, stay_s, overshoot_limit=None)`;
- `Mode(sim, name)`, configured with `.allow(from, to)` and `.dwell(mode, min_s, max_s)`.

A step without the call doesn't count for the rule.

**Signal rules** read an output (or input) by name after every step. `active` names one that is
non-zero while the rule applies:

- `Oscillation(sim, name, signal, amplitude, duration_s, active=None)`;
- `Wear(sim, name, signal, max_reversals_per_h, max_travel_per_h, deadband, active)`;
- `Rate(sim, name, signal, max_rate, active)`;
- `Energy(sim, name, signal, factor, budget, active)`;
- `Percent(sim, name, signal, min_percent, active)`.

**Conditions** carry a margin: `at_most`, `at_least`, `between`, `near` and `implies`. Combine them
with `&`, `|` and `~`; a plain bool has no margin.

**Checking a rule:** `rule.passed` and `rule.margin` give its verdict so far.

**Requirements:** `sim.verify_requirement(id)` names a requirement the simulation verifies:

- in `__init__` or `init()`, for the whole run;
- in `step()`, for that step.

The IDs are printed and recorded in the execution's `requirements` field and in
`cppmodel-requirements.json` (or `$CPPMODEL_REQUIREMENTS_FILE`).

See `documentation/Verification.md` of the CppModel SDK for the details of each rule.

## Workspace access

Every run reads its inputs from and records its execution in your workspace, so it needs to sign
in. For scripts and CI, set `CPPMODEL_USERNAME` and `CPPMODEL_PASSWORD`.

Without credentials, the first run prints a sign-in link and waits 60 seconds for you to sign in
(`CPPMODEL_LOGIN_TIMEOUT` changes that, in seconds); without a sign-in, `run()` raises. The link
goes to the process's standard output, which Jupyter doesn't show, so use credentials in notebooks.

## Fingerprint

Each execution records a `CppModel.BinaryFingerprint` of the simulation's code, so the workspace can
tell runs of the same code from runs of changed code. It is the fingerprint of the file your
`Simulation` subclass (or step function) is defined in. Set `sim.fingerprint_file` to use another
file.

## Errors

A failing library call raises `cppmodel.CppModelError`; its `status` is a `cppmodel.Status`.

## License

The CppModel library is proprietary software of Control Edge AB, used under the [CppModel Terms of Service](https://www.cppmodel.com/Tos.html); see `LICENSE`. The package also
contains third-party components under their own licenses, in `licenses/`.
