physiomodeler 0.5.1__py3-none-any.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.
@@ -0,0 +1,6 @@
1
+ from .physiomodel import PhysioModel
2
+ from . import post_analysis
3
+
4
+ __version__ = "0.5.1"
5
+
6
+ __all__ = ["PhysioModel", "post_analysis"]
@@ -0,0 +1,104 @@
1
+ import numpy as np
2
+ from numpy.typing import ArrayLike
3
+
4
+
5
+ def sanitize_inputs(
6
+ inputs,
7
+ allow_none=True,
8
+ allow_callable=False,
9
+ allow_string=False,
10
+ allow_sequence=False,
11
+ ):
12
+ if not isinstance(inputs, dict):
13
+ inputs = dict.fromkeys(inputs)
14
+
15
+ def check_value(key, value):
16
+ if not isinstance(key, str):
17
+ msg = f"Keys should be a non-string sequence, not '{key}' ({type(key)})"
18
+ raise TypeError(msg)
19
+
20
+ if (
21
+ (allow_none and value is None)
22
+ or isinstance(value, (int, float))
23
+ or (allow_callable and callable(value))
24
+ or (allow_string and isinstance(value, str))
25
+ or (allow_sequence and isinstance(value, (list, tuple)))
26
+ ):
27
+ return value
28
+
29
+ if isinstance(value, list):
30
+ value = np.array(value)
31
+
32
+ if isinstance(value, np.ndarray):
33
+ if np.sum(np.array(value.shape)) != 1:
34
+ raise ValueError("Input array should be 1D or reducable to 1D")
35
+
36
+ value = np.flatten(value)
37
+
38
+ return value
39
+
40
+ msg = f"Input with key '{key}' must be a {'function, ' if allow_callable else ''}number, list or 1D-array, not '{value}' ({type(value)})."
41
+ raise TypeError(msg)
42
+
43
+ return {key: check_value(key, value) for key, value in inputs.items()}
44
+
45
+
46
+ def parse_time(
47
+ time: float | tuple[float, float] | tuple[float, float, float] | list | np.ndarray,
48
+ dt: float | None = None,
49
+ ) -> tuple[float, float, np.ndarray]:
50
+ step = start = end = None
51
+
52
+ if isinstance(time, (float, int)):
53
+ start = 0
54
+ end = time
55
+ step = dt
56
+ elif isinstance(time, tuple) and len(time) == 2:
57
+ start = time[0] or 0
58
+ end = time[1]
59
+ step = dt
60
+ elif isinstance(time, tuple) and len(time) == 3:
61
+ if dt:
62
+ msg = "Multiple values for `dt`: can't parse `time` as a tuple with length 3 when `dt` is set."
63
+ raise ValueError(msg)
64
+ start = time[0] or 0
65
+ end = time[1]
66
+ step = time[2]
67
+ elif isinstance(time, tuple):
68
+ msg = f"`time` should a tuple with length 2 or 3, not {time}"
69
+ raise ValueError(msg)
70
+ elif isinstance(time, list):
71
+ time = np.array(time)
72
+
73
+ if isinstance(time, np.ndarray):
74
+ start = time[0]
75
+ end = time[-1]
76
+ elif step:
77
+ time = np.arange(start, end, step)
78
+ else:
79
+ time = None
80
+
81
+ if start is None or end is None:
82
+ raise ValueError("`time` cannot be parsed. No start and end are provided.")
83
+
84
+ return start, end, time
85
+
86
+
87
+ def convert_inputs(inputs: dict, time: int | ArrayLike, parameters):
88
+ """Converts function inputs to values at the given time point."""
89
+
90
+ parsed_inputs = dict()
91
+ for key, input_ in inputs.items():
92
+ if callable(input_):
93
+ parsed_inputs[key] = input_(time=time, parameters=parameters)
94
+ else:
95
+ parsed_inputs[key] = input_
96
+
97
+ if (
98
+ isinstance(parsed_inputs[key], np.ndarray)
99
+ and parsed_inputs[key].shape == tuple()
100
+ ):
101
+ # convert 0D array to number
102
+ parsed_inputs[key] = parsed_inputs[key].item()
103
+
104
+ return parsed_inputs
@@ -0,0 +1,40 @@
1
+ from typing import Literal
2
+
3
+
4
+ def state_crosses_value(
5
+ label: str,
6
+ value: float,
7
+ *,
8
+ terminal: bool = False,
9
+ direction: Literal[-1, 0, 1] = 0,
10
+ ):
11
+ """
12
+ Create an event that detects when a state crossing the given value.
13
+
14
+
15
+ Example:
16
+ # This example will have an event and terminate when the volume (state)
17
+ # crosses the value 0.
18
+
19
+ model = PhysioModeler(
20
+ ...,
21
+ events=[
22
+ state_crosses_value("volume", 0, terminal=True),
23
+ ]
24
+ )
25
+
26
+ Attrs:
27
+ label: label of the state to track
28
+ value: the event will trigger when the state crosses this value
29
+ terminal: whether to terminate the simulation if the event occurs
30
+ direction: if 0, any crossing will be detected; if 1, only positive
31
+ crossings (value goes from negative to positive) will be detected; if
32
+ -1, only negative crossings will be detected.
33
+ """
34
+
35
+ def event(time, state, inputs, parameters):
36
+ return state[label] - value
37
+
38
+ event.terminal = terminal
39
+ event.direction = direction
40
+ return event
@@ -0,0 +1,397 @@
1
+ from functools import reduce
2
+ from typing import Final, Literal, TypeAlias
3
+ from collections.abc import Callable
4
+
5
+ import numpy as np
6
+ import scipy as sp
7
+ import pandas as pd
8
+
9
+ from attrs import define, field, validators
10
+
11
+ from .converters import sanitize_inputs, parse_time, convert_inputs
12
+
13
+ ModelFunc: TypeAlias = Callable[[float | np.ndarray, dict, dict, dict], dict]
14
+
15
+ STATE_DERIVATIVE_LABEL_TEMPLATE: Final = "d{label}"
16
+
17
+
18
+ @define()
19
+ class PhysioModel:
20
+ """A state-space model described by an update function and parameters.
21
+
22
+ This class can be used to describe and numerically analyze a state space
23
+ model. A state-space model has at least one state variable that evolves
24
+ over time based on the state, as well as the inputs to and parameters of
25
+ the model.
26
+
27
+ The model is described by one or multiple update functions that returns how
28
+ the state changes. An update function should have the signature `fun(time,
29
+ state, inputs, parameters)`. Update functions are called in the order they
30
+ are provided. The output of each update function is added to the input of
31
+ later update functions. Each update function should return a dictionary
32
+ containing state derivatives and other value outputs. State derivates can
33
+ be returned by different functions, but a derivative should be returned for
34
+ all state variables.
35
+
36
+ By default, the key for a state derivative should be "d" + the state label,
37
+ e.g. "dvelocity" for the state "velocity". Alternatively, you can supply a
38
+ mapping dictionary `state_derivative_label_map` where the key is the state
39
+ variable label and the value is the state derivative label.
40
+
41
+ Inputs and parameters both contain values that can be used by the update
42
+ function. There is, however, a distinction between inputs and parameters.
43
+ Parameters are fixed values that are independent of time, e.g. the mass of
44
+ an object at the end of a pendulum. Inputs are disturbances added to the
45
+ system, and can be either fixed or variable with time, e.g. the force
46
+ applied to a moving mass. `inputs` and `parameters` are both dictionaries.
47
+ `inputs` values can be functions with the signature `fun(time, inputs,
48
+ parameters)`, which are converted to the appropriate before being passed to
49
+ the update function.
50
+
51
+ Events are functions with a signature `fun(time, state, inputs,
52
+ parameters)`. An event occurs when a function returns `0`. The solver will
53
+ find an accurate value of `time` where `fun(time, ...) == 0`. This ensures
54
+ proper simulation around that time point. Each event function can be marked
55
+ 'terminal' by adding the `terminal` attribute with value `True` to the
56
+ event function (`fun.terminal = True`). This results in the simulation
57
+ being terminal as soon as the event return value crosses `0`. See
58
+ [`scipy.integrate.solve_ivp`](https://docs.scipy.org/doc/scipy-1.14.1/reference/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp)
59
+ for more details.
60
+
61
+ Post analysis functions can be supplied, which receive the output of the
62
+ system as a pandas DataFrame (see `input_output_response`) and either
63
+ update the dataframe or return an array, list, Series or DataFrame object
64
+ with the same length that is added to the output of the model.
65
+
66
+ ### Example
67
+
68
+ The example below is the classic undampended pendulum. The system is
69
+ governed by the second derivative of the angle theta. The system state has
70
+ two components: the current angle "theta" and the angular velocity
71
+ "dtheta". The function `pendulum()` calculates the second derivative and
72
+ returns both the first and second derivative. When this system is solved,
73
+ the new state is calculated based on the derivatives of the previous step.
74
+
75
+ Example:
76
+ ```
77
+ >>> def pendulum(time, state, inputs, parameters):
78
+ ... g = parameters["g"]
79
+ ... l = parameters["l"]
80
+ ... theta = state["theta"]
81
+ ...
82
+ ... first_derivative_theta = state["dtheta"]
83
+ ... second_derivative_theta = -(g / l) * np.sin(theta)
84
+ ...
85
+ ... return {
86
+ ... "dtheta": first_derivative_theta,
87
+ ... "ddtheta": second_derivative_theta
88
+ ... }
89
+ >>> model = PhysioModel(
90
+ ... function=pendulum,
91
+ ... state=("theta", "dtheta"),
92
+ ... parameters={"g": 9.81, "l": 1},
93
+ ... )
94
+ >>> df = model.input_output_response(time=10, initial_state=(0.1, 0))
95
+ ```
96
+
97
+ Args:
98
+ function: a (list of) functions with accepting time, state, inputs and
99
+ parameters as arguments and returning a dictionary containing (at
100
+ least) the state derivatives state: a list containing the state labels,
101
+ or a dictionary containing the intial values of the state inputs: a
102
+ list/tuple containing the input labels, or a dictionary containing the
103
+ inputs parameters: a dictionary containing the parameters of the model
104
+ post_analysis: optional, a dictionary containing functions to calculate
105
+ values based on the output of the model state_derivative_label_map:
106
+ optional, a dictionary mapping the labels of the state derivatives to
107
+ the labels of the state variables.
108
+
109
+ """
110
+
111
+ function: ModelFunc | list[ModelFunc]
112
+ state: dict[str, float | None] = field(
113
+ kw_only=True,
114
+ converter=lambda x: sanitize_inputs(x, allow_callable=False),
115
+ validator=validators.min_len(1),
116
+ )
117
+ inputs: dict[str, float | Callable | None] = field(
118
+ kw_only=True,
119
+ factory=dict,
120
+ converter=lambda x: sanitize_inputs(x, allow_callable=True),
121
+ )
122
+ parameters: dict[str, float | str | bool] = field(kw_only=True, factory=dict)
123
+ events: Callable | list[Callable] = field(kw_only=True, factory=list)
124
+ post_analysis: dict[str, Callable] = field(kw_only=True, factory=dict)
125
+
126
+ state_derivative_label_map: dict[str, str] = field(kw_only=True, factory=dict)
127
+
128
+ @property
129
+ def input_labels(self) -> tuple:
130
+ """Labels of the inputs of the model."""
131
+ return tuple(self.inputs)
132
+
133
+ @property
134
+ def state_labels(self) -> tuple:
135
+ """Labels of the state variables of the model."""
136
+ return tuple(self.state)
137
+
138
+ @property
139
+ def state_derivative_labels(self) -> tuple:
140
+ """Labels of the state derivatives of the model."""
141
+ default_labels = {
142
+ label: STATE_DERIVATIVE_LABEL_TEMPLATE.format(label=label)
143
+ for label in self.state_labels
144
+ }
145
+ updated_labels = default_labels | self.state_derivative_label_map
146
+ return tuple(updated_labels.values())
147
+
148
+ def input_output_response(
149
+ self,
150
+ *,
151
+ initial_state: dict | list | tuple | None = None,
152
+ inputs: dict | list | tuple | None = None,
153
+ parameters: dict | list | tuple | None = None,
154
+ time: float | tuple | np.ndarray,
155
+ dt: float | None = None,
156
+ solve_ivp_method: Literal["Radau"] = "Radau",
157
+ rtol=1e-4,
158
+ atol=1e-6,
159
+ ):
160
+ """Generate an output based on the given initial state, inputs and
161
+ parameters.
162
+
163
+ This function generates the output of the model over a given time axis
164
+ based on an initial state, the inputs and parameters. If no or not all
165
+ the `initial_state`, `inputs` or `parameters` values are given, the
166
+ values provided to the system at initialisation are used instead.
167
+
168
+ The time can be passed in several ways. The simplest is passing a
169
+ duration. The model will be solved up to that duration. When `time` is
170
+ a tuple of length 2, it indicates the start and end time. (Note that
171
+ the initial state is the state at `t=start`, not at `t=0`.) The time
172
+ step will be `dt` if supplied, or determined by the solver otherwise.
173
+ If `time` is a tuple of length 3, the last value is used as the time
174
+ step. If a list or array is supplied, the output will contain these
175
+ time points.
176
+
177
+ Relative and abolute tolerance for the solver. The solver estimates a
178
+ local error as `atol + rol * abs(state)`. `rtol` controls a relative
179
+ accuracy ('number of correct digits') while `atol` controls abolute
180
+ accuracy ('number of correct decimal places'). See
181
+ [`scipy.integrate.solve_ivp`](https://docs.scipy.org/doc/scipy-1.14.1/reference/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp)
182
+ for more details.
183
+
184
+ Args:
185
+ initial_state: optional, initial values for the state as a
186
+ dictionary, or list/tuple of values
187
+ inputs: optional, inputs of the system as a dictionary or
188
+ list/tuple of values
189
+ parameters:
190
+ optional, dictionary with the parameters of the system
191
+ time:
192
+ int/float, tuple or array indicating the time axis of the output
193
+ dt: optional, the time step of the time axis
194
+ solve_ivp_method: solver used; currently only "Radau" is supported
195
+ rtol: float, relative tolerance
196
+ atol: float: absolute tolerance
197
+ """
198
+
199
+ inputs = self._parse_inputs(inputs)
200
+ parameters = self._parse_parameters(parameters)
201
+ initial_state = self._parse_state(initial_state)
202
+ (t_start, t_end, time) = parse_time(time, dt)
203
+ events = self._parse_events(inputs, parameters)
204
+
205
+ update_function: Callable
206
+
207
+ if isinstance(self.function, list):
208
+ functions: list = self.function
209
+
210
+ def multiple_functions_wrapper(time, state, inputs, parameters) -> dict:
211
+ updating_inputs = inputs.copy()
212
+ outputs = []
213
+ for function in functions:
214
+ out = function(time, state, updating_inputs, parameters)
215
+ updating_inputs |= out
216
+ outputs.append(out)
217
+
218
+ # combine all dictionaries
219
+ return reduce(lambda a, b: {**a, **b}, outputs)
220
+
221
+ update_function = multiple_functions_wrapper
222
+ elif callable(self.function):
223
+ update_function = self.function
224
+ else:
225
+ msg = "Provided argument `function` is not a function or list of functions."
226
+ raise TypeError(msg)
227
+
228
+ def function_wrapper(
229
+ time, state, inputs=inputs, parameters=parameters, function=update_function
230
+ ) -> dict:
231
+ """Wraps the user function to provide proper inputs and add inputs
232
+ and state to the results."""
233
+ converted_inputs = convert_inputs(inputs, time, parameters)
234
+
235
+ state = {k: v for k, v in zip(self.state_labels, state)}
236
+ result = function(time, state, converted_inputs, parameters)
237
+
238
+ result = converted_inputs | state | result
239
+
240
+ return result
241
+
242
+ def simplified_func(time, state, inputs=inputs, parameters=parameters) -> tuple:
243
+ output = function_wrapper(time, state, inputs, parameters)
244
+ return tuple(output[label] for label in self.state_derivative_labels)
245
+
246
+ solution = sp.integrate.solve_ivp(
247
+ simplified_func,
248
+ (t_start, t_end),
249
+ [initial_state[k] for k in self.state_labels],
250
+ method=solve_ivp_method,
251
+ t_eval=time,
252
+ events=events,
253
+ rtol=rtol,
254
+ atol=atol,
255
+ )
256
+
257
+ # generate an output for each timepoint/state combination
258
+ all_outputs = list(
259
+ function_wrapper(time, state)
260
+ for time, state in zip(solution.t, solution.y.T)
261
+ )
262
+ output_keys = all_outputs[0].keys()
263
+
264
+ # zip all outputs
265
+ zipped_outputs = {k: [o[k] for o in all_outputs] for k in output_keys}
266
+
267
+ if "time" not in zipped_outputs:
268
+ zipped_outputs["time"] = solution.t
269
+
270
+ data_frame = pd.DataFrame(zipped_outputs).set_index("time")
271
+
272
+ for k, update_function in self.post_analysis.items():
273
+ post_analysis_result = update_function(data_frame, parameters)
274
+ if post_analysis_result is None:
275
+ pass
276
+ elif isinstance(post_analysis_result, pd.DataFrame):
277
+ # TODO: join with copy of orignal, not original
278
+ data_frame = data_frame.join(post_analysis_result)
279
+ else:
280
+ try:
281
+ data_frame[k] = post_analysis_result
282
+ except: # noqa: E722
283
+ msg = "The result of post analysis can't seem to be added to the output."
284
+ raise RuntimeError(msg)
285
+
286
+ data_frame.attrs = dict(solution)
287
+
288
+ return data_frame
289
+
290
+ def _parse_events(self, inputs, parameters):
291
+ if self.events is None:
292
+ return []
293
+
294
+ def event_wrapper(event):
295
+ if not callable(event):
296
+ msg = f"'{event}' ({type(event)}) is not callable."
297
+ raise TypeError(msg)
298
+
299
+ def internal(time, state):
300
+ state = dict(zip(self.state_labels, state))
301
+ return event(
302
+ time=time, state=state, inputs=inputs, parameters=parameters
303
+ )
304
+
305
+ if hasattr(event, "terminal"):
306
+ internal.terminal = event.terminal
307
+
308
+ if hasattr(event, "direction"):
309
+ internal.direction = event.direction
310
+
311
+ return internal
312
+
313
+ events = self.events if isinstance(self.events, list) else [self.events]
314
+ events = [event_wrapper(event) for event in events]
315
+ return events
316
+
317
+ def _parse_state(self, state) -> dict:
318
+ if state is None:
319
+ state = {}
320
+ elif isinstance(state, list | tuple):
321
+ state = {k: v for k, v in zip(self.state_labels, state, strict=True)}
322
+
323
+ if not isinstance(state, dict):
324
+ msg = f"Initial state should be dict or sequence, not {type(state)}"
325
+ raise TypeError(msg)
326
+
327
+ state = self.state | state
328
+ state = sanitize_inputs(state, allow_none=False)
329
+ return state
330
+
331
+ def _parse_parameters(self, parameters):
332
+ if parameters is None:
333
+ parameters = {}
334
+
335
+ parameters = self.parameters | parameters
336
+ parameters = sanitize_inputs(
337
+ parameters, allow_string=True, allow_none=False, allow_sequence=True
338
+ )
339
+ return parameters
340
+
341
+ def _parse_inputs(self, inputs):
342
+ if inputs is None:
343
+ inputs = {}
344
+
345
+ elif isinstance(inputs, (list, tuple, np.ndarray)):
346
+ if len(inputs) != len(self.input_labels):
347
+ msg = f"The number of inputs ({len(inputs)}) does not match the number of defined inputs ({len(self.input_labels)})."
348
+ raise ValueError(msg)
349
+ inputs = {k: v for k, v in zip(self.input_labels, inputs, strict=True)}
350
+
351
+ if not isinstance(inputs, dict):
352
+ raise TypeError(f"Inputs should be dict or sequence, not {type(inputs)}")
353
+
354
+ inputs = self.inputs | inputs
355
+ inputs = sanitize_inputs(inputs, allow_callable=True, allow_none=False)
356
+ return inputs
357
+
358
+ def find_equilibrium_state(
359
+ self,
360
+ *,
361
+ time: float,
362
+ dt: float | None = None,
363
+ estimated_equilibrium_state: dict | None = None,
364
+ inputs: dict | None = None,
365
+ parameters: dict | None = None,
366
+ max_n_runs: int = 100,
367
+ rtol: float = 1e-4,
368
+ atol: float = 1e-6,
369
+ rtol_eq: float = 1e-3,
370
+ ):
371
+ for _ in range(max_n_runs):
372
+ if estimated_equilibrium_state is None:
373
+ estimated_equilibrium_state = {k: 0 for k in self.state_labels}
374
+
375
+ data = self.input_output_response(
376
+ time=time,
377
+ dt=dt,
378
+ initial_state=estimated_equilibrium_state,
379
+ inputs=inputs,
380
+ parameters=parameters,
381
+ atol=atol,
382
+ rtol=rtol,
383
+ )
384
+ final_state = dict(data.loc[:, self.state_labels].iloc[-1])
385
+
386
+ if np.allclose(
387
+ list(estimated_equilibrium_state.values()),
388
+ list(final_state.values()),
389
+ rtol=rtol_eq,
390
+ ):
391
+ return final_state
392
+
393
+ estimated_equilibrium_state = final_state
394
+
395
+ else:
396
+ msg = f"Could not find an equilibrium in {max_n_runs} iterations."
397
+ raise RuntimeError(msg)
@@ -0,0 +1,16 @@
1
+ import numpy as np
2
+
3
+
4
+ def mean_value(data, name, window=np.blackman(1200), trim=True):
5
+ diff = np.diff(data["time"])
6
+ mean_diff = np.mean(diff)
7
+ if not np.allclose(diff, mean_diff) or mean_diff <= 0:
8
+ msg = "Time should be linearly increasing."
9
+ raise ValueError(msg)
10
+ normalized_window = window / np.sum(window)
11
+ convolution = np.convolve(data[name], normalized_window, mode="same")
12
+ if trim:
13
+ len_window = len(normalized_window) // 2
14
+ convolution[:len_window] = np.nan
15
+ convolution[-len_window:] = np.nan
16
+ return convolution
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.3
2
+ Name: physiomodeler
3
+ Version: 0.5.1
4
+ Summary: Create physiological models for educational purposes.
5
+ Author-email: Peter Somhorst <p.somhorst@tudelft.nl>
6
+ License: The MIT License (MIT)
7
+
8
+ Copyright (c) 2023 Peter Somhorst
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in
18
+ all copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ THE SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: education,modelling,physiology,respiration,simulation
29
+ Classifier: License :: OSI Approved :: MIT License
30
+ Requires-Python: >=3.11
31
+ Requires-Dist: attrs
32
+ Requires-Dist: matplotlib
33
+ Requires-Dist: numpy
34
+ Requires-Dist: pandas
35
+ Requires-Dist: scipy
36
+ Provides-Extra: dev
37
+ Requires-Dist: hatch; extra == 'dev'
38
+ Requires-Dist: hatchling; extra == 'dev'
39
+ Provides-Extra: docs
40
+ Requires-Dist: black; extra == 'docs'
41
+ Requires-Dist: mike; extra == 'docs'
42
+ Requires-Dist: mkdocs; extra == 'docs'
43
+ Requires-Dist: mkdocs-exclude; extra == 'docs'
44
+ Requires-Dist: mkdocs-material; extra == 'docs'
45
+ Requires-Dist: mkdocstrings; extra == 'docs'
46
+ Requires-Dist: mkdocstrings-python; extra == 'docs'
47
+ Requires-Dist: pymdown-extensions; extra == 'docs'
@@ -0,0 +1,9 @@
1
+ physiomodeler/__init__.py,sha256=cSetgQEkVt1-u5uMZFdnqSayMCtokXGyp2i47XHQywg,132
2
+ physiomodeler/converters.py,sha256=H8iszsw2wKZLHeeNNk9xTXXY77byiq0wNGwQUZT8EZ8,3185
3
+ physiomodeler/events.py,sha256=Cxf2fpolGgZmwtafh1ZjNWT7UY0x3ER5SVIE1zHrc9c,1111
4
+ physiomodeler/physiomodel.py,sha256=LmcUDJADhBHOL8-7IyrCcQZ404CYTVFYmNNvfnQltkY,16342
5
+ physiomodeler/post_analysis.py,sha256=7GFgxQwHpeSAv12ODLRD49S8XTmxHeZm7g7BnwgKX0Y,582
6
+ physiomodeler-0.5.1.dist-info/METADATA,sha256=zerxnxYcm1mIdzqeIqQgEGQhVPa-nN92UuQh6j10530,2171
7
+ physiomodeler-0.5.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
8
+ physiomodeler-0.5.1.dist-info/licenses/LICENSE,sha256=bU80a4iDhEPFZUtLA0WQPguH14WlVDm8gA-CkhHH0LY,1081
9
+ physiomodeler-0.5.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Peter Somhorst
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
13
+ all 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
21
+ THE SOFTWARE.