mxlpy 0.15.0__py3-none-any.whl → 0.17.0__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.
- mxlpy/__init__.py +4 -1
- mxlpy/fns.py +513 -21
- mxlpy/integrators/int_assimulo.py +2 -1
- mxlpy/mc.py +84 -70
- mxlpy/mca.py +97 -98
- mxlpy/meta/codegen_latex.py +279 -14
- mxlpy/meta/source_tools.py +122 -4
- mxlpy/model.py +50 -24
- mxlpy/npe/__init__.py +38 -0
- mxlpy/npe/_torch.py +436 -0
- mxlpy/report.py +33 -6
- mxlpy/sbml/_import.py +5 -2
- mxlpy/scan.py +40 -38
- mxlpy/surrogates/__init__.py +7 -6
- mxlpy/surrogates/_poly.py +12 -9
- mxlpy/surrogates/_torch.py +137 -43
- mxlpy/symbolic/strikepy.py +1 -3
- mxlpy/types.py +18 -5
- {mxlpy-0.15.0.dist-info → mxlpy-0.17.0.dist-info}/METADATA +5 -4
- {mxlpy-0.15.0.dist-info → mxlpy-0.17.0.dist-info}/RECORD +22 -21
- mxlpy/npe.py +0 -277
- {mxlpy-0.15.0.dist-info → mxlpy-0.17.0.dist-info}/WHEEL +0 -0
- {mxlpy-0.15.0.dist-info → mxlpy-0.17.0.dist-info}/licenses/LICENSE +0 -0
mxlpy/npe/_torch.py
ADDED
@@ -0,0 +1,436 @@
|
|
1
|
+
"""Neural Network Parameter Estimation (NPE) Module.
|
2
|
+
|
3
|
+
This module provides classes and functions for training neural network models to estimate
|
4
|
+
parameters in metabolic models. It includes functionality for both steady-state and
|
5
|
+
time-series data.
|
6
|
+
|
7
|
+
Functions:
|
8
|
+
train_torch_surrogate: Train a PyTorch surrogate model
|
9
|
+
train_torch_time_course_estimator: Train a PyTorch time course estimator
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
from dataclasses import dataclass
|
15
|
+
from pathlib import Path
|
16
|
+
from typing import TYPE_CHECKING, Self, cast
|
17
|
+
|
18
|
+
import numpy as np
|
19
|
+
import pandas as pd
|
20
|
+
import torch
|
21
|
+
import tqdm
|
22
|
+
from torch import nn
|
23
|
+
from torch.optim.adam import Adam
|
24
|
+
|
25
|
+
from mxlpy.nn._torch import LSTM, MLP, DefaultDevice
|
26
|
+
from mxlpy.parallel import Cache
|
27
|
+
from mxlpy.types import AbstractEstimator
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from collections.abc import Callable
|
31
|
+
|
32
|
+
from torch.optim.optimizer import ParamsT
|
33
|
+
|
34
|
+
DefaultCache = Cache(Path(".cache"))
|
35
|
+
|
36
|
+
type LossFn = Callable[[torch.Tensor, torch.Tensor], torch.Tensor]
|
37
|
+
|
38
|
+
__all__ = [
|
39
|
+
"DefaultCache",
|
40
|
+
"LossFn",
|
41
|
+
"TorchSteadyState",
|
42
|
+
"TorchSteadyStateTrainer",
|
43
|
+
"TorchTimeCourse",
|
44
|
+
"TorchTimeCourseTrainer",
|
45
|
+
"train_torch_steady_state",
|
46
|
+
"train_torch_time_course",
|
47
|
+
]
|
48
|
+
|
49
|
+
|
50
|
+
def _mean_abs(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
51
|
+
"""Standard loss for surrogates.
|
52
|
+
|
53
|
+
Args:
|
54
|
+
x: Predictions of a model.
|
55
|
+
y: Targets.
|
56
|
+
|
57
|
+
Returns:
|
58
|
+
torch.Tensor: loss.
|
59
|
+
|
60
|
+
"""
|
61
|
+
return torch.mean(torch.abs(x - y))
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass(kw_only=True)
|
65
|
+
class TorchSteadyState(AbstractEstimator):
|
66
|
+
"""Estimator for steady state data using PyTorch models."""
|
67
|
+
|
68
|
+
model: torch.nn.Module
|
69
|
+
|
70
|
+
def predict(self, features: pd.Series | pd.DataFrame) -> pd.DataFrame:
|
71
|
+
"""Predict the target values for the given features."""
|
72
|
+
with torch.no_grad():
|
73
|
+
pred = self.model(torch.tensor(features.to_numpy(), dtype=torch.float32))
|
74
|
+
return pd.DataFrame(pred, columns=self.parameter_names)
|
75
|
+
|
76
|
+
|
77
|
+
@dataclass(kw_only=True)
|
78
|
+
class TorchTimeCourse(AbstractEstimator):
|
79
|
+
"""Estimator for time course data using PyTorch models."""
|
80
|
+
|
81
|
+
model: torch.nn.Module
|
82
|
+
|
83
|
+
def predict(self, features: pd.Series | pd.DataFrame) -> pd.DataFrame:
|
84
|
+
"""Predict the target values for the given features."""
|
85
|
+
idx = cast(pd.MultiIndex, features.index)
|
86
|
+
features_ = torch.Tensor(
|
87
|
+
np.swapaxes(
|
88
|
+
features.to_numpy().reshape(
|
89
|
+
(
|
90
|
+
len(idx.levels[0]),
|
91
|
+
len(idx.levels[1]),
|
92
|
+
len(features.columns),
|
93
|
+
)
|
94
|
+
),
|
95
|
+
axis1=0,
|
96
|
+
axis2=1,
|
97
|
+
),
|
98
|
+
)
|
99
|
+
with torch.no_grad():
|
100
|
+
pred = self.model(features_)
|
101
|
+
return pd.DataFrame(pred, columns=self.parameter_names)
|
102
|
+
|
103
|
+
|
104
|
+
@dataclass
|
105
|
+
class TorchSteadyStateTrainer:
|
106
|
+
"""Trainer for steady state data using PyTorch models."""
|
107
|
+
|
108
|
+
features: pd.DataFrame
|
109
|
+
targets: pd.DataFrame
|
110
|
+
approximator: nn.Module
|
111
|
+
optimimzer: Adam
|
112
|
+
device: torch.device
|
113
|
+
losses: list[pd.Series]
|
114
|
+
loss_fn: LossFn
|
115
|
+
|
116
|
+
def __init__(
|
117
|
+
self,
|
118
|
+
features: pd.DataFrame,
|
119
|
+
targets: pd.DataFrame,
|
120
|
+
approximator: nn.Module | None = None,
|
121
|
+
optimimzer_cls: Callable[[ParamsT], Adam] = Adam,
|
122
|
+
device: torch.device = DefaultDevice,
|
123
|
+
loss_fn: LossFn = _mean_abs,
|
124
|
+
) -> None:
|
125
|
+
"""Initialize the trainer with features, targets, and model.
|
126
|
+
|
127
|
+
Args:
|
128
|
+
features: DataFrame containing the input features for training
|
129
|
+
targets: DataFrame containing the target values for training
|
130
|
+
approximator: Predefined neural network model (None to use default MLP)
|
131
|
+
optimimzer_cls: Optimizer class to use for training (default: Adam)
|
132
|
+
device: Device to run the training on (default: DefaultDevice)
|
133
|
+
loss_fn: Loss function
|
134
|
+
|
135
|
+
"""
|
136
|
+
self.features = features
|
137
|
+
self.targets = targets
|
138
|
+
|
139
|
+
if approximator is None:
|
140
|
+
n_hidden = max(2 * len(features.columns) * len(targets.columns), 10)
|
141
|
+
n_outputs = len(targets.columns)
|
142
|
+
approximator = MLP(
|
143
|
+
n_inputs=len(features.columns),
|
144
|
+
neurons_per_layer=[n_hidden, n_hidden, n_outputs],
|
145
|
+
)
|
146
|
+
self.approximator = approximator.to(device)
|
147
|
+
self.optimizer = optimimzer_cls(approximator.parameters())
|
148
|
+
self.device = device
|
149
|
+
self.loss_fn = loss_fn
|
150
|
+
self.losses = []
|
151
|
+
|
152
|
+
def train(
|
153
|
+
self,
|
154
|
+
epochs: int,
|
155
|
+
batch_size: int | None = None,
|
156
|
+
) -> Self:
|
157
|
+
"""Train the model using the provided features and targets.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
epochs: Number of training epochs
|
161
|
+
batch_size: Size of mini-batches for training (None for full-batch)
|
162
|
+
|
163
|
+
"""
|
164
|
+
features = torch.Tensor(self.features.to_numpy(), device=self.device)
|
165
|
+
targets = torch.Tensor(self.targets.to_numpy(), device=self.device)
|
166
|
+
|
167
|
+
if batch_size is None:
|
168
|
+
losses = _train_full(
|
169
|
+
approximator=self.approximator,
|
170
|
+
features=features,
|
171
|
+
targets=targets,
|
172
|
+
epochs=epochs,
|
173
|
+
optimizer=self.optimizer,
|
174
|
+
loss_fn=self.loss_fn,
|
175
|
+
)
|
176
|
+
else:
|
177
|
+
losses = _train_batched(
|
178
|
+
approximator=self.approximator,
|
179
|
+
features=features,
|
180
|
+
targets=targets,
|
181
|
+
epochs=epochs,
|
182
|
+
optimizer=self.optimizer,
|
183
|
+
batch_size=batch_size,
|
184
|
+
loss_fn=self.loss_fn,
|
185
|
+
)
|
186
|
+
|
187
|
+
if len(self.losses) > 0:
|
188
|
+
losses.index += self.losses[-1].index[-1]
|
189
|
+
self.losses.append(losses)
|
190
|
+
return self
|
191
|
+
|
192
|
+
def get_loss(self) -> pd.Series:
|
193
|
+
"""Get the loss history of the training process."""
|
194
|
+
return pd.concat(self.losses)
|
195
|
+
|
196
|
+
def get_estimator(self) -> TorchSteadyState:
|
197
|
+
"""Get the trained estimator."""
|
198
|
+
return TorchSteadyState(
|
199
|
+
model=self.approximator,
|
200
|
+
parameter_names=list(self.targets.columns),
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
@dataclass
|
205
|
+
class TorchTimeCourseTrainer:
|
206
|
+
"""Trainer for time course data using PyTorch models."""
|
207
|
+
|
208
|
+
features: pd.DataFrame
|
209
|
+
targets: pd.DataFrame
|
210
|
+
approximator: nn.Module
|
211
|
+
optimimzer: Adam
|
212
|
+
device: torch.device
|
213
|
+
losses: list[pd.Series]
|
214
|
+
loss_fn: LossFn
|
215
|
+
|
216
|
+
def __init__(
|
217
|
+
self,
|
218
|
+
features: pd.DataFrame,
|
219
|
+
targets: pd.DataFrame,
|
220
|
+
approximator: nn.Module | None = None,
|
221
|
+
optimimzer_cls: Callable[[ParamsT], Adam] = Adam,
|
222
|
+
device: torch.device = DefaultDevice,
|
223
|
+
loss_fn: LossFn = _mean_abs,
|
224
|
+
) -> None:
|
225
|
+
"""Initialize the trainer with features, targets, and model.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
features: DataFrame containing the input features for training
|
229
|
+
targets: DataFrame containing the target values for training
|
230
|
+
approximator: Predefined neural network model (None to use default LSTM)
|
231
|
+
optimimzer_cls: Optimizer class to use for training (default: Adam)
|
232
|
+
device: Device to run the training on (default: DefaultDevice)
|
233
|
+
loss_fn: Loss function
|
234
|
+
|
235
|
+
"""
|
236
|
+
self.features = features
|
237
|
+
self.targets = targets
|
238
|
+
|
239
|
+
if approximator is None:
|
240
|
+
approximator = LSTM(
|
241
|
+
n_inputs=len(features.columns),
|
242
|
+
n_outputs=len(targets.columns),
|
243
|
+
n_hidden=1,
|
244
|
+
).to(device)
|
245
|
+
self.approximator = approximator.to(device)
|
246
|
+
self.optimizer = optimimzer_cls(approximator.parameters())
|
247
|
+
self.device = device
|
248
|
+
self.loss_fn = loss_fn
|
249
|
+
self.losses = []
|
250
|
+
|
251
|
+
def train(
|
252
|
+
self,
|
253
|
+
epochs: int,
|
254
|
+
batch_size: int | None = None,
|
255
|
+
) -> Self:
|
256
|
+
"""Train the model using the provided features and targets.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
epochs: Number of training epochs
|
260
|
+
batch_size: Size of mini-batches for training (None for full-batch)
|
261
|
+
|
262
|
+
"""
|
263
|
+
features = torch.Tensor(
|
264
|
+
np.swapaxes(
|
265
|
+
self.features.to_numpy().reshape(
|
266
|
+
(len(self.targets), -1, len(self.features.columns))
|
267
|
+
),
|
268
|
+
axis1=0,
|
269
|
+
axis2=1,
|
270
|
+
),
|
271
|
+
device=self.device,
|
272
|
+
)
|
273
|
+
targets = torch.Tensor(self.targets.to_numpy(), device=self.device)
|
274
|
+
|
275
|
+
if batch_size is None:
|
276
|
+
losses = _train_full(
|
277
|
+
approximator=self.approximator,
|
278
|
+
features=features,
|
279
|
+
targets=targets,
|
280
|
+
epochs=epochs,
|
281
|
+
optimizer=self.optimizer,
|
282
|
+
loss_fn=self.loss_fn,
|
283
|
+
)
|
284
|
+
else:
|
285
|
+
losses = _train_batched(
|
286
|
+
approximator=self.approximator,
|
287
|
+
features=features,
|
288
|
+
targets=targets,
|
289
|
+
epochs=epochs,
|
290
|
+
optimizer=self.optimizer,
|
291
|
+
batch_size=batch_size,
|
292
|
+
loss_fn=self.loss_fn,
|
293
|
+
)
|
294
|
+
|
295
|
+
if len(self.losses) > 0:
|
296
|
+
losses.index += self.losses[-1].index[-1]
|
297
|
+
self.losses.append(losses)
|
298
|
+
return self
|
299
|
+
|
300
|
+
def get_loss(self) -> pd.Series:
|
301
|
+
"""Get the loss history of the training process."""
|
302
|
+
return pd.concat(self.losses)
|
303
|
+
|
304
|
+
def get_estimator(self) -> TorchTimeCourse:
|
305
|
+
"""Get the trained estimator."""
|
306
|
+
return TorchTimeCourse(
|
307
|
+
model=self.approximator,
|
308
|
+
parameter_names=list(self.targets.columns),
|
309
|
+
)
|
310
|
+
|
311
|
+
|
312
|
+
def _train_batched(
|
313
|
+
approximator: nn.Module,
|
314
|
+
features: torch.Tensor,
|
315
|
+
targets: torch.Tensor,
|
316
|
+
epochs: int,
|
317
|
+
optimizer: Adam,
|
318
|
+
batch_size: int,
|
319
|
+
loss_fn: LossFn,
|
320
|
+
) -> pd.Series:
|
321
|
+
losses = {}
|
322
|
+
for epoch in tqdm.trange(epochs):
|
323
|
+
permutation = torch.randperm(features.size()[0])
|
324
|
+
epoch_loss = 0
|
325
|
+
for i in range(0, features.size()[0], batch_size):
|
326
|
+
optimizer.zero_grad()
|
327
|
+
indices = permutation[i : i + batch_size]
|
328
|
+
loss = loss_fn(approximator(features[indices]), targets[indices])
|
329
|
+
loss.backward()
|
330
|
+
optimizer.step()
|
331
|
+
epoch_loss += loss.detach().numpy()
|
332
|
+
|
333
|
+
losses[epoch] = epoch_loss / (features.size()[0] / batch_size)
|
334
|
+
return pd.Series(losses, dtype=float)
|
335
|
+
|
336
|
+
|
337
|
+
def _train_full(
|
338
|
+
approximator: nn.Module,
|
339
|
+
features: torch.Tensor,
|
340
|
+
targets: torch.Tensor,
|
341
|
+
epochs: int,
|
342
|
+
optimizer: Adam,
|
343
|
+
loss_fn: LossFn,
|
344
|
+
) -> pd.Series:
|
345
|
+
losses = {}
|
346
|
+
for i in tqdm.trange(epochs):
|
347
|
+
optimizer.zero_grad()
|
348
|
+
loss = loss_fn(approximator(features), targets)
|
349
|
+
loss.backward()
|
350
|
+
optimizer.step()
|
351
|
+
losses[i] = loss.detach().numpy()
|
352
|
+
return pd.Series(losses, dtype=float)
|
353
|
+
|
354
|
+
|
355
|
+
def train_torch_steady_state(
|
356
|
+
features: pd.DataFrame,
|
357
|
+
targets: pd.DataFrame,
|
358
|
+
epochs: int,
|
359
|
+
batch_size: int | None = None,
|
360
|
+
approximator: nn.Module | None = None,
|
361
|
+
optimimzer_cls: Callable[[ParamsT], Adam] = Adam,
|
362
|
+
device: torch.device = DefaultDevice,
|
363
|
+
) -> tuple[TorchSteadyState, pd.Series]:
|
364
|
+
"""Train a PyTorch steady state estimator.
|
365
|
+
|
366
|
+
This function trains a neural network model to estimate steady state data
|
367
|
+
using the provided features and targets. It supports both full-batch and
|
368
|
+
mini-batch training.
|
369
|
+
|
370
|
+
Examples:
|
371
|
+
>>> train_torch_ss_estimator(features, targets, epochs=100)
|
372
|
+
|
373
|
+
Args:
|
374
|
+
features: DataFrame containing the input features for training
|
375
|
+
targets: DataFrame containing the target values for training
|
376
|
+
epochs: Number of training epochs
|
377
|
+
batch_size: Size of mini-batches for training (None for full-batch)
|
378
|
+
approximator: Predefined neural network model (None to use default MLP)
|
379
|
+
optimimzer_cls: Optimizer class to use for training (default: Adam)
|
380
|
+
device: Device to run the training on (default: DefaultDevice)
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
tuple[TorchTimeSeriesEstimator, pd.Series]: Trained estimator and loss history
|
384
|
+
|
385
|
+
"""
|
386
|
+
trainer = TorchSteadyStateTrainer(
|
387
|
+
features=features,
|
388
|
+
targets=targets,
|
389
|
+
approximator=approximator,
|
390
|
+
optimimzer_cls=optimimzer_cls,
|
391
|
+
device=device,
|
392
|
+
).train(epochs=epochs, batch_size=batch_size)
|
393
|
+
|
394
|
+
return trainer.get_estimator(), trainer.get_loss()
|
395
|
+
|
396
|
+
|
397
|
+
def train_torch_time_course(
|
398
|
+
features: pd.DataFrame,
|
399
|
+
targets: pd.DataFrame,
|
400
|
+
epochs: int,
|
401
|
+
batch_size: int | None = None,
|
402
|
+
approximator: nn.Module | None = None,
|
403
|
+
optimimzer_cls: Callable[[ParamsT], Adam] = Adam,
|
404
|
+
device: torch.device = DefaultDevice,
|
405
|
+
) -> tuple[TorchTimeCourse, pd.Series]:
|
406
|
+
"""Train a PyTorch time course estimator.
|
407
|
+
|
408
|
+
This function trains a neural network model to estimate time course data
|
409
|
+
using the provided features and targets. It supports both full-batch and
|
410
|
+
mini-batch training.
|
411
|
+
|
412
|
+
Examples:
|
413
|
+
>>> train_torch_time_course_estimator(features, targets, epochs=100)
|
414
|
+
|
415
|
+
Args:
|
416
|
+
features: DataFrame containing the input features for training
|
417
|
+
targets: DataFrame containing the target values for training
|
418
|
+
epochs: Number of training epochs
|
419
|
+
batch_size: Size of mini-batches for training (None for full-batch)
|
420
|
+
approximator: Predefined neural network model (None to use default LSTM)
|
421
|
+
optimimzer_cls: Optimizer class to use for training (default: Adam)
|
422
|
+
device: Device to run the training on (default: DefaultDevice)
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
tuple[TorchTimeSeriesEstimator, pd.Series]: Trained estimator and loss history
|
426
|
+
|
427
|
+
"""
|
428
|
+
trainer = TorchTimeCourseTrainer(
|
429
|
+
features=features,
|
430
|
+
targets=targets,
|
431
|
+
approximator=approximator,
|
432
|
+
optimimzer_cls=optimimzer_cls,
|
433
|
+
device=device,
|
434
|
+
).train(epochs=epochs, batch_size=batch_size)
|
435
|
+
|
436
|
+
return trainer.get_estimator(), trainer.get_loss()
|
mxlpy/report.py
CHANGED
@@ -48,12 +48,39 @@ def markdown(
|
|
48
48
|
) -> str:
|
49
49
|
"""Generate a markdown report comparing two models.
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
51
|
+
Parameters
|
52
|
+
----------
|
53
|
+
m1
|
54
|
+
The first model to compare
|
55
|
+
m2
|
56
|
+
The second model to compare
|
57
|
+
analyses
|
58
|
+
A list of functions that analyze both models and return a report section with image
|
59
|
+
rel_change
|
60
|
+
The relative change threshold for numerical differences
|
61
|
+
img_path
|
62
|
+
The path to save images
|
63
|
+
|
64
|
+
Returns
|
65
|
+
-------
|
66
|
+
str
|
67
|
+
Markdown formatted report comparing the two models
|
68
|
+
|
69
|
+
Examples
|
70
|
+
--------
|
71
|
+
>>> from mxlpy import Model
|
72
|
+
>>> m1 = Model().add_parameter("k1", 0.1).add_variable("S", 1.0)
|
73
|
+
>>> m2 = Model().add_parameter("k1", 0.2).add_variable("S", 1.0)
|
74
|
+
>>> report = markdown(m1, m2)
|
75
|
+
>>> "Parameters" in report and "k1" in report
|
76
|
+
True
|
77
|
+
|
78
|
+
>>> # With custom analysis function
|
79
|
+
>>> def custom_analysis(m1, m2, path):
|
80
|
+
... return "## Custom analysis", path / "image.png"
|
81
|
+
>>> report = markdown(m1, m2, analyses=[custom_analysis])
|
82
|
+
>>> "Custom analysis" in report
|
83
|
+
True
|
57
84
|
|
58
85
|
"""
|
59
86
|
content: list[str] = [
|
mxlpy/sbml/_import.py
CHANGED
@@ -12,7 +12,7 @@ import libsbml
|
|
12
12
|
import numpy as np # noqa: F401 # models might need it
|
13
13
|
import sympy
|
14
14
|
|
15
|
-
from mxlpy.model import Model, _sort_dependencies
|
15
|
+
from mxlpy.model import Dependency, Model, _sort_dependencies
|
16
16
|
from mxlpy.paths import default_tmp_dir
|
17
17
|
from mxlpy.sbml._data import (
|
18
18
|
AtomicUnit,
|
@@ -522,7 +522,10 @@ def _codgen(name: str, sbml: Parser) -> Path:
|
|
522
522
|
^ set(variables)
|
523
523
|
^ set(sbml.derived)
|
524
524
|
| {"time"},
|
525
|
-
elements=[
|
525
|
+
elements=[
|
526
|
+
Dependency(name=k, required=set(v.args), provided={k})
|
527
|
+
for k, v in sbml.initial_assignment.items()
|
528
|
+
],
|
526
529
|
)
|
527
530
|
|
528
531
|
if len(initial_assignment_order) > 0:
|