cppmodel 0.9.0__py3-none-manylinux_2_28_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cppmodel/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """CppModel for Python: write CppModel simulations in Python and submit them to your workspace."""
2
+ from ._native import API_VERSION, lib as _lib, library_path
3
+ from ._simulation import CppModelError, Inputs, Outputs, Parameters, Simulation, Status
4
+
5
+ __version__ = "0.9.0"
6
+
7
+
8
+ def library_version():
9
+ """The release version of the loaded CppModel library, e.g. "0.8.0"."""
10
+ return _lib.cm_version().decode("utf-8")
11
+
12
+
13
+ __all__ = [
14
+ "API_VERSION",
15
+ "CppModelError",
16
+ "Inputs",
17
+ "Outputs",
18
+ "Parameters",
19
+ "Simulation",
20
+ "Status",
21
+ "library_path",
22
+ "library_version",
23
+ ]
cppmodel/_native.py ADDED
@@ -0,0 +1,99 @@
1
+ """ctypes declarations of the CppModel C API (include/cppmodel.h) and loading of the shared library."""
2
+ import ctypes
3
+ import os
4
+ import sys
5
+ from ctypes import POINTER, c_char_p, c_double, c_int, c_size_t, c_uint64, c_void_p
6
+
7
+ # The major C API version these bindings are written against; the library must report the same.
8
+ API_VERSION = 1
9
+
10
+ # cm_callback: cm_status (*)(cm_simulation simulation, uint64_t time_ms, void *user_data)
11
+ CALLBACK = ctypes.CFUNCTYPE(c_int, c_void_p, c_uint64, c_void_p)
12
+
13
+ _LIBRARY_NAMES = {"win32": "cppmodel.dll", "darwin": "libcppmodel.dylib"}
14
+
15
+
16
+ def library_path():
17
+ """The shared library to load: $CPPMODEL_LIBRARY, else the one bundled in the package."""
18
+ override = os.environ.get("CPPMODEL_LIBRARY")
19
+ if override:
20
+ return override
21
+ name = _LIBRARY_NAMES.get(sys.platform, "libcppmodel.so.1")
22
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib", name)
23
+
24
+
25
+ _handle = c_void_p
26
+ _out_handle = POINTER(c_void_p)
27
+ _out_text = POINTER(c_char_p)
28
+ _out_int = POINTER(c_int)
29
+ _out_size = POINTER(c_size_t)
30
+ _out_double = POINTER(c_double)
31
+ _out_uint64 = POINTER(c_uint64)
32
+
33
+ # Only the functions the bindings use. Persistent simulations are C/C++ only, and
34
+ # cm_require_non_interactive is left out because it exits the process.
35
+ _SIGNATURES = {
36
+ "cm_api_version": (c_int, []),
37
+ "cm_version": (c_char_p, []),
38
+ "cm_last_error_message": (c_char_p, []),
39
+ "cm_simulation_create": (c_int, [c_char_p, c_uint64, c_uint64, _out_handle]),
40
+ "cm_simulation_destroy": (c_int, [_handle]),
41
+ "cm_simulation_set_name": (c_int, [_handle, c_char_p]),
42
+ "cm_simulation_get_name": (c_int, [_handle, _out_text]),
43
+ "cm_simulation_set_timing": (c_int, [_handle, c_uint64, c_uint64]),
44
+ "cm_simulation_get_timing": (c_int, [_handle, _out_uint64, _out_uint64]),
45
+ "cm_simulation_set_run_offline": (c_int, [_handle, c_int]),
46
+ "cm_simulation_set_fingerprint_file": (c_int, [_handle, c_char_p]),
47
+ "cm_simulation_set_init_callback": (c_int, [_handle, CALLBACK, c_void_p]),
48
+ "cm_simulation_set_step_callback": (c_int, [_handle, CALLBACK, c_void_p]),
49
+ "cm_simulation_set_finalize_callback": (c_int, [_handle, CALLBACK, c_void_p]),
50
+ "cm_simulation_run": (c_int, [_handle]),
51
+ "cm_simulation_reset_data": (c_int, [_handle]),
52
+ "cm_simulation_get_result": (c_int, [_handle, _out_int]),
53
+ "cm_simulation_set_result": (c_int, [_handle, c_int]),
54
+ "cm_simulation_results_json": (c_int, [_handle, _out_text]),
55
+ "cm_simulation_apply_inputs_json": (c_int, [_handle, c_char_p]),
56
+ "cm_simulation_inputs": (c_int, [_handle, _out_handle]),
57
+ "cm_simulation_outputs": (c_int, [_handle, _out_handle]),
58
+ "cm_input_get": (c_int, [_handle, c_char_p, c_double, _out_double]),
59
+ "cm_inputs_load_json": (c_int, [_handle, c_char_p]),
60
+ "cm_inputs_contains": (c_int, [_handle, c_char_p, _out_int]),
61
+ "cm_inputs_count": (c_int, [_handle, _out_size]),
62
+ "cm_inputs_key_at": (c_int, [_handle, c_size_t, _out_text]),
63
+ "cm_output_set": (c_int, [_handle, c_char_p, c_double]),
64
+ "cm_output_get": (c_int, [_handle, c_char_p, _out_double]),
65
+ "cm_outputs_contains": (c_int, [_handle, c_char_p, _out_int]),
66
+ "cm_outputs_count": (c_int, [_handle, _out_size]),
67
+ "cm_outputs_key_at": (c_int, [_handle, c_size_t, _out_text]),
68
+ "cm_param_set_double": (c_int, [_handle, c_char_p, c_double]),
69
+ "cm_param_set_json": (c_int, [_handle, c_char_p, c_char_p]),
70
+ "cm_param_get_json": (c_int, [_handle, c_char_p, _out_text]),
71
+ "cm_param_get_or_json": (c_int, [_handle, c_char_p, c_char_p, _out_text]),
72
+ "cm_param_contains": (c_int, [_handle, c_char_p, _out_int]),
73
+ "cm_param_count": (c_int, [_handle, _out_size]),
74
+ "cm_param_key_at": (c_int, [_handle, c_size_t, _out_text]),
75
+ }
76
+
77
+
78
+ def _load():
79
+ path = library_path()
80
+ try:
81
+ library = ctypes.CDLL(path)
82
+ except OSError as error:
83
+ raise ImportError(
84
+ f"cppmodel: cannot load the CppModel library {path}: {error}. "
85
+ "Reinstall the cppmodel package for this platform, or set CPPMODEL_LIBRARY."
86
+ ) from error
87
+ for name, (restype, argtypes) in _SIGNATURES.items():
88
+ function = getattr(library, name)
89
+ function.restype = restype
90
+ function.argtypes = argtypes
91
+ if library.cm_api_version() != API_VERSION:
92
+ raise ImportError(
93
+ f"cppmodel: {path} implements C API version {library.cm_api_version()}, "
94
+ f"these bindings need version {API_VERSION}"
95
+ )
96
+ return library
97
+
98
+
99
+ lib = _load()
@@ -0,0 +1,391 @@
1
+ """A Pythonic wrapper over the C API: Simulation with its inputs, outputs and parameters."""
2
+ import enum
3
+ import inspect
4
+ import json
5
+ import os
6
+ import sys
7
+ from ctypes import byref, c_char_p, c_double, c_int, c_size_t, c_uint64, c_void_p
8
+
9
+ from ._native import CALLBACK, lib
10
+
11
+
12
+ class Status(enum.IntEnum):
13
+ """cm_status codes of the C API."""
14
+
15
+ OK = 0
16
+ INVALID_ARGUMENT = 1
17
+ NOT_FOUND = 2
18
+ TYPE_MISMATCH = 3
19
+ INVALID_JSON = 4
20
+ CALLBACK = 5
21
+ INTERNAL = 6
22
+
23
+
24
+ class CppModelError(Exception):
25
+ """A CppModel library call failed. `status` is the Status it returned."""
26
+
27
+ def __init__(self, status, message):
28
+ super().__init__(message)
29
+ try:
30
+ self.status = Status(status)
31
+ except ValueError:
32
+ self.status = status
33
+
34
+
35
+ def _check(status):
36
+ if status != Status.OK:
37
+ raise CppModelError(status, lib.cm_last_error_message().decode("utf-8", "replace"))
38
+
39
+
40
+ def _encode(text):
41
+ return text.encode("utf-8")
42
+
43
+
44
+ def _decode(raw):
45
+ return raw.decode("utf-8") if raw is not None else ""
46
+
47
+
48
+ def _keys(count_function, key_function, handle):
49
+ count = c_size_t()
50
+ _check(count_function(handle, byref(count)))
51
+ keys = []
52
+ for index in range(count.value):
53
+ key = c_char_p()
54
+ _check(key_function(handle, index, byref(key)))
55
+ keys.append(_decode(key.value))
56
+ return keys
57
+
58
+
59
+ class Inputs:
60
+ """The simulation's inputs: time series whose value is read at the current step."""
61
+
62
+ def __init__(self, simulation):
63
+ self._simulation = simulation
64
+ handle = c_void_p()
65
+ _check(lib.cm_simulation_inputs(simulation._live(), byref(handle)))
66
+ self._handle = handle
67
+
68
+ def _live(self):
69
+ self._simulation._live()
70
+ return self._handle
71
+
72
+ def get(self, name, fallback=0.0):
73
+ """The input's value at the current step, or `fallback` (then recorded as its value) when
74
+ nothing was loaded for it."""
75
+ value = c_double()
76
+ _check(lib.cm_input_get(self._live(), _encode(name), float(fallback), byref(value)))
77
+ return value.value
78
+
79
+ def __getitem__(self, name):
80
+ return self.get(name)
81
+
82
+ def load(self, series):
83
+ """Loads series [{"label": name, "x": [ms, ...], "y": [value, ...]}, ...]."""
84
+ _check(lib.cm_inputs_load_json(self._live(), _encode(json.dumps(series))))
85
+
86
+ def __contains__(self, name):
87
+ found = c_int()
88
+ _check(lib.cm_inputs_contains(self._live(), _encode(name), byref(found)))
89
+ return bool(found.value)
90
+
91
+ def keys(self):
92
+ """The inputs read or loaded so far, in name order."""
93
+ return _keys(lib.cm_inputs_count, lib.cm_inputs_key_at, self._live())
94
+
95
+ def __iter__(self):
96
+ return iter(self.keys())
97
+
98
+ def __len__(self):
99
+ count = c_size_t()
100
+ _check(lib.cm_inputs_count(self._live(), byref(count)))
101
+ return count.value
102
+
103
+
104
+ class Outputs:
105
+ """The simulation's outputs. An output holds its value until set again and is recorded at the
106
+ end of every step. Values are numbers; bools are stored as 1 and 0."""
107
+
108
+ def __init__(self, simulation):
109
+ self._simulation = simulation
110
+ handle = c_void_p()
111
+ _check(lib.cm_simulation_outputs(simulation._live(), byref(handle)))
112
+ self._handle = handle
113
+
114
+ def _live(self):
115
+ self._simulation._live()
116
+ return self._handle
117
+
118
+ def __setitem__(self, name, value):
119
+ _check(lib.cm_output_set(self._live(), _encode(name), float(value)))
120
+
121
+ def __getitem__(self, name):
122
+ """The output's current value; reading an output that was never set creates it with 0."""
123
+ value = c_double()
124
+ _check(lib.cm_output_get(self._live(), _encode(name), byref(value)))
125
+ return value.value
126
+
127
+ def __contains__(self, name):
128
+ found = c_int()
129
+ _check(lib.cm_outputs_contains(self._live(), _encode(name), byref(found)))
130
+ return bool(found.value)
131
+
132
+ def keys(self):
133
+ return _keys(lib.cm_outputs_count, lib.cm_outputs_key_at, self._live())
134
+
135
+ def __iter__(self):
136
+ return iter(self.keys())
137
+
138
+ def __len__(self):
139
+ count = c_size_t()
140
+ _check(lib.cm_outputs_count(self._live(), byref(count)))
141
+ return count.value
142
+
143
+
144
+ class Parameters:
145
+ """The simulation's parameters. They keep their JSON type: numbers, bools, strings, lists,
146
+ dicts or None."""
147
+
148
+ def __init__(self, simulation):
149
+ self._simulation = simulation
150
+
151
+ def get(self, name, fallback):
152
+ """The parameter's value, storing `fallback` first when it isn't set. A fetched value
153
+ replaces a stored fallback; a value set with parameters[name] = ... is never replaced."""
154
+ out = c_char_p()
155
+ fallback_json = _encode(json.dumps(fallback, allow_nan=False))
156
+ _check(lib.cm_param_get_or_json(self._simulation._live(), _encode(name), fallback_json, byref(out)))
157
+ return json.loads(_decode(out.value))
158
+
159
+ def __getitem__(self, name):
160
+ out = c_char_p()
161
+ status = lib.cm_param_get_json(self._simulation._live(), _encode(name), byref(out))
162
+ if status == Status.NOT_FOUND:
163
+ raise KeyError(name)
164
+ _check(status)
165
+ return json.loads(_decode(out.value))
166
+
167
+ def __setitem__(self, name, value):
168
+ handle = self._simulation._live()
169
+ if isinstance(value, float):
170
+ _check(lib.cm_param_set_double(handle, _encode(name), value))
171
+ else:
172
+ _check(lib.cm_param_set_json(handle, _encode(name), _encode(json.dumps(value, allow_nan=False))))
173
+
174
+ def __contains__(self, name):
175
+ found = c_int()
176
+ _check(lib.cm_param_contains(self._simulation._live(), _encode(name), byref(found)))
177
+ return bool(found.value)
178
+
179
+ def keys(self):
180
+ return _keys(lib.cm_param_count, lib.cm_param_key_at, self._simulation._live())
181
+
182
+ def __iter__(self):
183
+ return iter(self.keys())
184
+
185
+ def __len__(self):
186
+ count = c_size_t()
187
+ _check(lib.cm_param_count(self._simulation._live(), byref(count)))
188
+ return count.value
189
+
190
+ def to_dict(self):
191
+ return {name: self[name] for name in self.keys()}
192
+
193
+
194
+ class Simulation:
195
+ """A CppModel simulation.
196
+
197
+ Either subclass it and override init(), step(time_ms) and finalize(), or pass those as
198
+ functions taking the simulation as their first argument:
199
+
200
+ sim = Simulation("Heater", total_time_ms=60000, step_ms=100, step=lambda sim, t: ...)
201
+
202
+ run() fetches the pending inputs and parameters from the workspace, calls init(), then step()
203
+ for every step from 0 to total_time_ms, then finalize(), and submits the execution. An
204
+ exception raised in init(), step() or finalize() aborts the run (nothing is submitted) and is
205
+ re-raised by run().
206
+ """
207
+
208
+ def __init__(self, name, total_time_ms, step_ms, *, offline=False, init=None, step=None, finalize=None):
209
+ handle = c_void_p()
210
+ _check(lib.cm_simulation_create(_encode(name), total_time_ms, step_ms, byref(handle)))
211
+ self._handle = handle
212
+ self._error = None
213
+ self._functions = {"init": init, "step": step, "finalize": finalize}
214
+ # ctypes frees a callback when its CFUNCTYPE object is collected, so keep them with the handle.
215
+ self._callbacks = (
216
+ CALLBACK(lambda _sim, _time, _user: self._invoke(self.init)),
217
+ CALLBACK(lambda _sim, time_ms, _user: self._invoke(self.step, time_ms)),
218
+ CALLBACK(lambda _sim, _time, _user: self._invoke(self.finalize)),
219
+ )
220
+ _check(lib.cm_simulation_set_init_callback(handle, self._callbacks[0], None))
221
+ _check(lib.cm_simulation_set_step_callback(handle, self._callbacks[1], None))
222
+ _check(lib.cm_simulation_set_finalize_callback(handle, self._callbacks[2], None))
223
+ self._offline = False
224
+ self.offline = offline
225
+ self._fingerprint_file = None
226
+ default = self._default_fingerprint_file()
227
+ if default is not None and lib.cm_simulation_set_fingerprint_file(handle, _encode(default)) == Status.OK:
228
+ self._fingerprint_file = default
229
+ self.inputs = Inputs(self)
230
+ self.outputs = Outputs(self)
231
+ self.parameters = Parameters(self)
232
+
233
+ # ── Overridables ─────────────────────────────────────────────────────
234
+
235
+ def init(self):
236
+ """Called once before the first step."""
237
+ if self._functions["init"] is not None:
238
+ self._functions["init"](self)
239
+
240
+ def step(self, time_ms):
241
+ """Called every step with the simulation time in milliseconds."""
242
+ if self._functions["step"] is not None:
243
+ self._functions["step"](self, time_ms)
244
+
245
+ def finalize(self):
246
+ """Called once after the last step."""
247
+ if self._functions["finalize"] is not None:
248
+ self._functions["finalize"](self)
249
+
250
+ # ── Running ──────────────────────────────────────────────────────────
251
+
252
+ def _invoke(self, method, *args):
253
+ try:
254
+ method(*args)
255
+ return Status.OK
256
+ except BaseException as error: # also KeyboardInterrupt: Ctrl-C aborts the run between steps
257
+ self._error = error
258
+ return Status.CALLBACK
259
+
260
+ def run(self):
261
+ """Runs the simulation and returns its result: 0 when every step passed, otherwise the
262
+ CppModel.Step of the first failing step (or -1)."""
263
+ self._error = None
264
+ status = lib.cm_simulation_run(self._live())
265
+ if self._error is not None:
266
+ error, self._error = self._error, None
267
+ raise error
268
+ _check(status)
269
+ return self.result
270
+
271
+ def reset_data(self):
272
+ """Deletes the simulation on the server (same as deleting it in the UI). Call before run()."""
273
+ _check(lib.cm_simulation_reset_data(self._live()))
274
+
275
+ def results(self):
276
+ """The execution as it is (or would be) submitted: {"inputs", "results", "parameters", ...}."""
277
+ out = c_char_p()
278
+ _check(lib.cm_simulation_results_json(self._live(), byref(out)))
279
+ return json.loads(_decode(out.value))
280
+
281
+ def apply_inputs(self, response):
282
+ """Applies a GET /simulations/{id}/inputs response ({"inputs": [...], "parameters": {...}}).
283
+ Inputs and parameters already set are kept."""
284
+ _check(lib.cm_simulation_apply_inputs_json(self._live(), _encode(json.dumps(response))))
285
+
286
+ # ── Properties ───────────────────────────────────────────────────────
287
+
288
+ @property
289
+ def name(self):
290
+ out = c_char_p()
291
+ _check(lib.cm_simulation_get_name(self._live(), byref(out)))
292
+ return _decode(out.value)
293
+
294
+ @name.setter
295
+ def name(self, value):
296
+ _check(lib.cm_simulation_set_name(self._live(), _encode(value)))
297
+
298
+ def _timing(self):
299
+ total, step = c_uint64(), c_uint64()
300
+ _check(lib.cm_simulation_get_timing(self._live(), byref(total), byref(step)))
301
+ return total.value, step.value
302
+
303
+ @property
304
+ def total_time_ms(self):
305
+ return self._timing()[0]
306
+
307
+ @total_time_ms.setter
308
+ def total_time_ms(self, value):
309
+ _check(lib.cm_simulation_set_timing(self._live(), value, self.step_ms))
310
+
311
+ @property
312
+ def step_ms(self):
313
+ return self._timing()[1]
314
+
315
+ @step_ms.setter
316
+ def step_ms(self, value):
317
+ _check(lib.cm_simulation_set_timing(self._live(), self.total_time_ms, value))
318
+
319
+ @property
320
+ def offline(self):
321
+ """When true, run() doesn't reach the workspace: nothing is fetched or submitted."""
322
+ return self._offline
323
+
324
+ @offline.setter
325
+ def offline(self, value):
326
+ _check(lib.cm_simulation_set_run_offline(self._live(), 1 if value else 0))
327
+ self._offline = bool(value)
328
+
329
+ def _default_fingerprint_file(self):
330
+ # The simulation's code: the subclass's file, else the callbacks' file, else the main script.
331
+ candidates = [type(self)] if type(self) is not Simulation else []
332
+ candidates += [function for function in self._functions.values() if function is not None]
333
+ for candidate in candidates:
334
+ try:
335
+ path = inspect.getsourcefile(candidate)
336
+ except TypeError:
337
+ continue
338
+ if path and os.path.isfile(path):
339
+ return os.path.abspath(path)
340
+ main = getattr(sys.modules.get("__main__"), "__file__", None)
341
+ return os.path.abspath(main) if main and os.path.isfile(main) else None
342
+
343
+ @property
344
+ def fingerprint_file(self):
345
+ """The file whose fingerprint is recorded as CppModel.BinaryFingerprint, so that runs of the
346
+ same code can be told apart from runs of changed code. Defaults to the file the simulation is
347
+ defined in; None means the Python interpreter's fingerprint."""
348
+ return self._fingerprint_file
349
+
350
+ @fingerprint_file.setter
351
+ def fingerprint_file(self, path):
352
+ encoded = _encode(os.path.abspath(path)) if path is not None else None
353
+ _check(lib.cm_simulation_set_fingerprint_file(self._live(), encoded))
354
+ self._fingerprint_file = os.path.abspath(path) if path is not None else None
355
+
356
+ @property
357
+ def result(self):
358
+ """0 when every step passed; otherwise the CppModel.Step of the first failing step (or -1)."""
359
+ out = c_int()
360
+ _check(lib.cm_simulation_get_result(self._live(), byref(out)))
361
+ return out.value
362
+
363
+ @result.setter
364
+ def result(self, value):
365
+ _check(lib.cm_simulation_set_result(self._live(), value))
366
+
367
+ @property
368
+ def passed(self):
369
+ return self.result == 0
370
+
371
+ # ── Lifetime ─────────────────────────────────────────────────────────
372
+
373
+ def _live(self):
374
+ if self._handle is None:
375
+ raise ValueError("the simulation is closed")
376
+ return self._handle
377
+
378
+ def close(self):
379
+ """Frees the simulation. It is also freed when garbage-collected."""
380
+ handle, self._handle = getattr(self, "_handle", None), None
381
+ if handle is not None:
382
+ lib.cm_simulation_destroy(handle)
383
+
384
+ def __enter__(self):
385
+ return self
386
+
387
+ def __exit__(self, *exc_info):
388
+ self.close()
389
+
390
+ def __del__(self):
391
+ self.close()
Binary file
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.5
2
+ Name: cppmodel
3
+ Version: 0.9.0
4
+ Summary: Write CppModel simulations in Python
5
+ Project-URL: Homepage, https://cppmodel.com
6
+ Author-email: Control Edge AB <support@cedge.se>
7
+ License-Expression: LicenseRef-CppModel
8
+ License-File: LICENSE
9
+ License-File: licenses/IXWebSocket-LICENSE.txt
10
+ License-File: licenses/cpp-httplib-LICENSE.txt
11
+ License-File: licenses/jwt-cpp-LICENSE.txt
12
+ License-File: licenses/nlohmann-json-LICENSE.txt
13
+ License-File: licenses/openssl-LICENSE.txt
14
+ License-File: licenses/picojson-LICENSE.txt
15
+ Keywords: cppmodel,embedded,plant model,simulation,testing
16
+ Classifier: Development Status :: 4 - Beta
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Operating System :: MacOS
19
+ Classifier: Operating System :: Microsoft :: Windows
20
+ Classifier: Operating System :: POSIX :: Linux
21
+ Classifier: Programming Language :: Python :: 3
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+
27
+ # CppModel for Python
28
+
29
+ Write [CppModel](https://cppmodel.com) simulations in Python. Every run fetches the inputs and
30
+ parameters posted for it in your CppModel workspace and submits its execution there, just like a
31
+ C or C++ simulation built with the CppModel SDK.
32
+
33
+ ```sh
34
+ pip install cppmodel
35
+ ```
36
+
37
+ Wheels are available for Linux (x86_64 and aarch64, glibc 2.28+), Windows (x64 and ARM64) and
38
+ macOS 11+ (Apple silicon and Intel), for Python 3.9 and later. Each wheel contains the CppModel
39
+ library, so nothing else needs installing.
40
+
41
+ ## A simulation
42
+
43
+ ```python
44
+ import sys
45
+ import cppmodel
46
+
47
+
48
+ class Heater(cppmodel.Simulation):
49
+ def init(self):
50
+ self.temperature = 20.0
51
+
52
+ def step(self, time_ms):
53
+ setpoint = self.inputs.get("Setpoint", 22.0) # input, with its fallback
54
+ gain = self.parameters.get("HeaterGain", 0.05) # parameter, with its fallback
55
+
56
+ heater_on = self.temperature < setpoint # the controller under test
57
+ self.temperature += gain if heater_on else -0.01
58
+
59
+ self.outputs["Temperature"] = self.temperature
60
+ self.outputs["HeaterOn"] = heater_on
61
+ self.outputs["CppModel.Step"] = time_ms
62
+ self.outputs["CppModel.StepResult"] = self.temperature < setpoint + 1.0
63
+
64
+
65
+ with Heater("Example - Heater (Python)", total_time_ms=60_000, step_ms=100) as simulation:
66
+ sys.exit(simulation.run())
67
+ ```
68
+
69
+ `run()` fetches the pending inputs and parameters, calls `init()`, then `step(time_ms)` for every
70
+ step from 0 to `total_time_ms`, then `finalize()`, and submits the execution. It returns 0 when every
71
+ step passed, otherwise the `CppModel.Step` of the first step whose `CppModel.StepResult` was 0.
72
+
73
+ An exception raised in `init()`, `step()` or `finalize()` (Ctrl-C included) aborts the run, submits
74
+ nothing and is re-raised by `run()`.
75
+
76
+ Instead of subclassing, you can pass functions that take the simulation as their first argument:
77
+
78
+ ```python
79
+ def ramp(sim, time_ms):
80
+ sim.outputs["Ramp"] = time_ms
81
+
82
+ sim = cppmodel.Simulation("Ramp", total_time_ms=1000, step_ms=100, step=ramp)
83
+ sim.run()
84
+ ```
85
+
86
+ ## Inputs, outputs and parameters
87
+
88
+ | | |
89
+ |---|---|
90
+ | `sim.inputs.get(name, fallback)` | The input's value at the current step, or `fallback` (recorded as its value) |
91
+ | `sim.inputs[name]` | Same, with fallback 0 |
92
+ | `sim.inputs.load([{"label", "x", "y"}, ...])` | Loads input series locally (x in ms) |
93
+ | `sim.outputs[name] = value` | Sets an output; it holds until set again and is recorded every step. Values are numbers; bools become 1/0 |
94
+ | `sim.parameters.get(name, fallback)` | The parameter, storing `fallback` when it isn't set; a fetched value replaces the fallback |
95
+ | `sim.parameters[name] = value` | Sets a parameter that fetched values never replace |
96
+ | `sim.parameters[name]` | The parameter, or `KeyError` |
97
+ | `sim.results()` | The execution as submitted: `{"inputs", "results", "parameters", ...}` |
98
+
99
+ Inputs and outputs are numbers. Parameters keep their JSON type: numbers, bools, strings, lists,
100
+ dicts and `None`. All three support `in`, `len()`, iteration and `keys()`.
101
+
102
+ ## Workspace access
103
+
104
+ A run needs to sign in to your workspace:
105
+
106
+ - **Scripts, CI:** set `CPPMODEL_USERNAME` and `CPPMODEL_PASSWORD`.
107
+ - **Without the workspace:** create the simulation with `offline=True`. Nothing is fetched or
108
+ submitted.
109
+
110
+ Without credentials, the first run prints a sign-in link and waits until you've signed in. The link
111
+ goes to the process's standard output, which Jupyter doesn't show, so use credentials in notebooks.
112
+
113
+ ## Fingerprint
114
+
115
+ Each execution records a `CppModel.BinaryFingerprint` of the simulation's code, so the workspace can
116
+ tell runs of the same code from runs of changed code. It is the fingerprint of the file your
117
+ `Simulation` subclass (or step function) is defined in. Set `sim.fingerprint_file` to use another
118
+ file.
119
+
120
+ ## Errors
121
+
122
+ A failing library call raises `cppmodel.CppModelError`; its `status` is a `cppmodel.Status`.
123
+
124
+ ## License
125
+
126
+ 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
127
+ contains third-party components under their own licenses, in `licenses/`.
@@ -0,0 +1,14 @@
1
+ cppmodel/__init__.py,sha256=ROu85RGFEeHsH8hSfT4qVk0Aud3EQoqF7hSI0tgZozw,594
2
+ cppmodel/_native.py,sha256=qUAZuxYVprNMVT0YEekZ7poJlMZkNgP2gyF42Lu09sg,4512
3
+ cppmodel/_simulation.py,sha256=2iogZUPmZO367SNlF7Wvxf253GYVTnHVPNe13UXhaPI,14502
4
+ cppmodel/lib/libcppmodel.so.1,sha256=hF165qMliq9SsePZDXzHhw3u-bJ3a50EQoRHZIvjs_Y,8680696
5
+ cppmodel-0.9.0.dist-info/METADATA,sha256=KF5E1BCwBiiWXhBdrI-gdiU02mhUDYgqiHBs83W4TWQ,5213
6
+ cppmodel-0.9.0.dist-info/WHEEL,sha256=oXgmTSTIbSDIyOVMzS8RHEg6vKUanNsrgMkxt107nkM,107
7
+ cppmodel-0.9.0.dist-info/licenses/LICENSE,sha256=m9J9CSAmn7L5LSEbmwtNhVbRCJin6u75y91XhYRNlgc,382
8
+ cppmodel-0.9.0.dist-info/licenses/licenses/IXWebSocket-LICENSE.txt,sha256=nMRtQmXxirlZt1xEECKF3AS7BOortpaxhY9cOPfrrKA,1506
9
+ cppmodel-0.9.0.dist-info/licenses/licenses/cpp-httplib-LICENSE.txt,sha256=S0XL4W17cbia5hJ-JuDZCgKRmMpelYrY49C4u-02TYs,1075
10
+ cppmodel-0.9.0.dist-info/licenses/licenses/jwt-cpp-LICENSE.txt,sha256=NcLX1oJKHh7SsmbsM0t4YnJq5ZOFXDz6aLlSu3XwgsQ,1075
11
+ cppmodel-0.9.0.dist-info/licenses/licenses/nlohmann-json-LICENSE.txt,sha256=RqZc_9HqlVEy2VqN2SFkBxSo1rU30uTkgtMRRa6VtgM,1076
12
+ cppmodel-0.9.0.dist-info/licenses/licenses/openssl-LICENSE.txt,sha256=QyBwV9TPsX2Di2V-9G25NfX992mLGmN8Hm8ENGqhsu0,611
13
+ cppmodel-0.9.0.dist-info/licenses/licenses/picojson-LICENSE.txt,sha256=VYX-FBzHuwjJU_OFnbYIhSlo0LvGJbm22VwL1jSbrLY,1336
14
+ cppmodel-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-manylinux_2_28_aarch64
@@ -0,0 +1,9 @@
1
+ CppModel
2
+ Copyright (c) Control Edge AB. All rights reserved.
3
+
4
+ This package and the CppModel library it contains are proprietary software of Control Edge AB.
5
+ They are licensed, not sold, and may only be used under the CppModel Terms of Service:
6
+ https://www.cppmodel.com/Tos.html
7
+
8
+ The package also contains third-party components, each under its own license; see the licenses
9
+ folder.
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are
5
+ met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright
8
+ notice, this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the
13
+ distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived
17
+ from this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 yhirose
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Dominik Thalhammer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013-2025 Niels Lohmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+
4
+ Copyright (c) OpenSSL Project Contributors
5
+
6
+ Licensed under the Apache License, Version 2.0 (the "License");
7
+ you may not use this file except in compliance with the License.
8
+ You may obtain a copy of the License at
9
+
10
+ https://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ Unless required by applicable law or agreed to in writing, software
13
+ distributed under the License is distributed on an "AS IS" BASIS,
14
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ See the License for the specific language governing permissions and
16
+ limitations under the License.
@@ -0,0 +1,25 @@
1
+ Copyright 2009-2010 Cybozu Labs, Inc.
2
+ Copyright 2011-2014 Kazuho Oku
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ POSSIBILITY OF SUCH DAMAGE.