modelbase2 0.5.0__py3-none-any.whl → 0.6.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.
@@ -329,7 +329,9 @@ class TexExport:
329
329
  parameters={gls.get(k, k): v for k, v in self.parameters.items()},
330
330
  variables={gls.get(k, k): v for k, v in self.variables.items()},
331
331
  derived={
332
- gls.get(k, k): Derived(fn=v.fn, args=[gls.get(i, i) for i in v.args])
332
+ gls.get(k, k): Derived(
333
+ name=k, fn=v.fn, args=[gls.get(i, i) for i in v.args]
334
+ )
333
335
  for k, v in self.derived.items()
334
336
  },
335
337
  reactions={
@@ -287,10 +287,12 @@ class LinearLabelMapper:
287
287
  stoichiometry = {}
288
288
  if substrate != "EXT":
289
289
  stoichiometry[substrate] = Derived(
290
- _neg_one_div, [substrate.split("__")[0]]
290
+ name=substrate, fn=_neg_one_div, args=[substrate.split("__")[0]]
291
291
  )
292
292
  if product != "EXT":
293
- stoichiometry[product] = Derived(_one_div, [product.split("__")[0]])
293
+ stoichiometry[product] = Derived(
294
+ name=product, fn=_one_div, args=[product.split("__")[0]]
295
+ )
294
296
 
295
297
  m.add_reaction(
296
298
  name=f"{rxn_name}__{i}",
modelbase2/model.py CHANGED
@@ -20,7 +20,6 @@ from modelbase2 import fns
20
20
  from modelbase2.types import (
21
21
  Array,
22
22
  Derived,
23
- Float,
24
23
  Reaction,
25
24
  Readout,
26
25
  )
@@ -238,6 +237,7 @@ class ModelCache:
238
237
  """
239
238
 
240
239
  var_names: list[str]
240
+ order: list[str]
241
241
  all_parameter_values: dict[str, float]
242
242
  derived_parameter_names: list[str]
243
243
  derived_variable_names: list[str]
@@ -293,22 +293,26 @@ class Model:
293
293
  # Sanity checks
294
294
  for name, el in it.chain(
295
295
  self._derived.items(),
296
- self._readouts.items(),
297
296
  self._reactions.items(),
297
+ self._readouts.items(),
298
298
  ):
299
299
  if not _check_function_arity(el.fn, len(el.args)):
300
300
  raise ArityMismatchError(name, el.fn, el.args)
301
301
 
302
- # Sort derived
303
- derived_order = _sort_dependencies(
302
+ # Sort derived & reactions
303
+ to_sort = self._derived | self._reactions | self._surrogates
304
+ order = _sort_dependencies(
304
305
  available=set(self._parameters) | set(self._variables) | {"time"},
305
- elements=[(k, set(v.args)) for k, v in self._derived.items()],
306
+ elements=[(k, set(v.args)) for k, v in to_sort.items()],
306
307
  )
307
308
 
308
309
  # Split derived into parameters and variables
310
+ # for user convenience
309
311
  derived_variable_names: list[str] = []
310
312
  derived_parameter_names: list[str] = []
311
- for name in derived_order:
313
+ for name in order:
314
+ if name in self._reactions or name in self._surrogates:
315
+ continue
312
316
  derived = self._derived[name]
313
317
  if all(i in all_parameter_names for i in derived.args):
314
318
  all_parameter_names.add(name)
@@ -348,6 +352,7 @@ class Model:
348
352
 
349
353
  self._cache = ModelCache(
350
354
  var_names=var_names,
355
+ order=order,
351
356
  all_parameter_values=all_parameter_values,
352
357
  stoich_by_cpds=stoich_by_compounds,
353
358
  dyn_stoich_by_cpds=dyn_stoich_by_compounds,
@@ -886,7 +891,7 @@ class Model:
886
891
 
887
892
  """
888
893
  self._insert_id(name=name, ctx="derived")
889
- self._derived[name] = Derived(fn, args)
894
+ self._derived[name] = Derived(name=name, fn=fn, args=args)
890
895
  return self
891
896
 
892
897
  def get_derived_parameter_names(self) -> list[str]:
@@ -995,7 +1000,7 @@ class Model:
995
1000
  """
996
1001
  if (cache := self._cache) is None:
997
1002
  cache = self._create_cache()
998
- args = self.get_args(concs=concs, time=time)
1003
+ args = self.get_dependent(concs=concs, time=time)
999
1004
 
1000
1005
  stoich_by_cpds = copy.deepcopy(cache.stoich_by_cpds)
1001
1006
  for cpd, stoich in cache.dyn_stoich_by_cpds.items():
@@ -1036,10 +1041,12 @@ class Model:
1036
1041
  self._insert_id(name=name, ctx="reaction")
1037
1042
 
1038
1043
  stoich: dict[str, Derived | float] = {
1039
- k: Derived(fns.constant, [v]) if isinstance(v, str) else v
1044
+ k: Derived(name=k, fn=fns.constant, args=[v]) if isinstance(v, str) else v
1040
1045
  for k, v in stoichiometry.items()
1041
1046
  }
1042
- self._reactions[name] = Reaction(fn=fn, stoichiometry=stoich, args=args)
1047
+ self._reactions[name] = Reaction(
1048
+ name=name, fn=fn, stoichiometry=stoich, args=args
1049
+ )
1043
1050
  return self
1044
1051
 
1045
1052
  def get_reaction_names(self) -> list[str]:
@@ -1088,7 +1095,9 @@ class Model:
1088
1095
 
1089
1096
  if stoichiometry is not None:
1090
1097
  stoich = {
1091
- k: Derived(fns.constant, [v]) if isinstance(v, str) else v
1098
+ k: Derived(name=k, fn=fns.constant, args=[v])
1099
+ if isinstance(v, str)
1100
+ else v
1092
1101
  for k, v in stoichiometry.items()
1093
1102
  }
1094
1103
  rxn.stoichiometry = stoich
@@ -1162,7 +1171,7 @@ class Model:
1162
1171
 
1163
1172
  """
1164
1173
  self._insert_id(name=name, ctx="readout")
1165
- self._readouts[name] = Readout(fn, args)
1174
+ self._readouts[name] = Readout(name=name, fn=fn, args=args)
1166
1175
  return self
1167
1176
 
1168
1177
  def get_readout_names(self) -> list[str]:
@@ -1283,26 +1292,31 @@ class Model:
1283
1292
  return self
1284
1293
 
1285
1294
  ##########################################################################
1286
- # Get args
1295
+ # Get dependent values. This includes
1296
+ # - derived parameters
1297
+ # - derived variables
1298
+ # - fluxes
1299
+ # - readouts
1287
1300
  ##########################################################################
1288
1301
 
1289
- def _get_args(
1302
+ def _get_dependent(
1290
1303
  self,
1291
1304
  concs: dict[str, float],
1292
1305
  time: float = 0.0,
1293
1306
  *,
1294
- include_readouts: bool,
1307
+ cache: ModelCache,
1295
1308
  ) -> dict[str, float]:
1296
- """Generate a dictionary of arguments for model calculations.
1309
+ """Generate a dictionary of model components dependent on other components.
1297
1310
 
1298
1311
  Examples:
1299
- >>> model._get_args({"x1": 1.0, "x2": 2.0}, time=0.0)
1312
+ >>> model._get_dependent({"x1": 1.0, "x2": 2.0}, time=0.0)
1300
1313
  {"x1": 1.0, "x2": 2.0, "k1": 0.1, "time": 0.0}
1301
1314
 
1302
1315
  Args:
1303
1316
  concs: A dictionary of concentrations with keys as the names of the substances
1304
1317
  and values as their respective concentrations.
1305
1318
  time: The time point for the calculation
1319
+ cache: A ModelCache object containing precomputed values and dependencies.
1306
1320
  include_readouts: A flag indicating whether to include readout values in the returned dictionary.
1307
1321
 
1308
1322
  Returns:
@@ -1311,23 +1325,16 @@ class Model:
1311
1325
  with their respective names as keys and their calculated values as values.
1312
1326
 
1313
1327
  """
1314
- if (cache := self._cache) is None:
1315
- cache = self._create_cache()
1316
-
1317
1328
  args: dict[str, float] = cache.all_parameter_values | concs
1318
1329
  args["time"] = time
1319
1330
 
1320
- derived = self._derived
1321
- for name in cache.derived_variable_names:
1322
- dv = derived[name]
1323
- args[name] = cast(float, dv.fn(*(args[arg] for arg in dv.args)))
1331
+ containers = self._derived | self._reactions | self._surrogates
1332
+ for name in cache.order:
1333
+ containers[name].calculate_inpl(args)
1324
1334
 
1325
- if include_readouts:
1326
- for name, ro in self._readouts.items():
1327
- args[name] = cast(float, ro.fn(*(args[arg] for arg in ro.args)))
1328
1335
  return args
1329
1336
 
1330
- def get_args(
1337
+ def get_dependent(
1331
1338
  self,
1332
1339
  concs: dict[str, float] | None = None,
1333
1340
  time: float = 0.0,
@@ -1358,16 +1365,22 @@ class Model:
1358
1365
  A pandas Series containing the generated arguments with float dtype.
1359
1366
 
1360
1367
  """
1361
- return pd.Series(
1362
- self._get_args(
1363
- concs=self.get_initial_conditions() if concs is None else concs,
1364
- time=time,
1365
- include_readouts=include_readouts,
1366
- ),
1367
- dtype=float,
1368
+ if (cache := self._cache) is None:
1369
+ cache = self._create_cache()
1370
+
1371
+ args = self._get_dependent(
1372
+ concs=self.get_initial_conditions() if concs is None else concs,
1373
+ time=time,
1374
+ cache=cache,
1368
1375
  )
1369
1376
 
1370
- def get_args_time_course(
1377
+ if include_readouts:
1378
+ for ro in self._readouts.values(): # FIXME: order?
1379
+ ro.calculate_inpl(args)
1380
+
1381
+ return pd.Series(args, dtype=float)
1382
+
1383
+ def get_dependent_time_course(
1371
1384
  self,
1372
1385
  concs: pd.DataFrame,
1373
1386
  *,
@@ -1410,10 +1423,9 @@ class Model:
1410
1423
  args = pd.concat((concs, pars_df), axis=1)
1411
1424
  args["time"] = args.index
1412
1425
 
1413
- derived = self._derived
1414
- for name in cache.derived_variable_names:
1415
- dv = derived[name]
1416
- args[name] = dv.fn(*args.loc[:, dv.args].to_numpy().T)
1426
+ containers = self._derived | self._reactions | self._surrogates
1427
+ for name in cache.order:
1428
+ containers[name].calculate_inpl_time_course(args)
1417
1429
 
1418
1430
  if include_readouts:
1419
1431
  for name, ro in self._readouts.items():
@@ -1421,48 +1433,91 @@ class Model:
1421
1433
  return args
1422
1434
 
1423
1435
  ##########################################################################
1424
- # Get full concs
1436
+ # Get args
1425
1437
  ##########################################################################
1426
1438
 
1427
- def get_full_concs(
1439
+ def get_args(
1428
1440
  self,
1429
1441
  concs: dict[str, float] | None = None,
1430
1442
  time: float = 0.0,
1431
1443
  *,
1432
- include_readouts: bool = True,
1444
+ include_derived: bool = True,
1445
+ include_readouts: bool = False,
1433
1446
  ) -> pd.Series:
1434
- """Get the full concentrations as a pandas Series.
1447
+ """Generate a pandas Series of arguments for the model.
1435
1448
 
1436
1449
  Examples:
1437
- >>> model.get_full_concs({"x1": 1.0, "x2": 2.0}, time=0.0)
1438
- pd.Series({
1439
- "x1": 1.0,
1440
- "x2": 2.0,
1441
- "d1": 3.0,
1442
- "d2": 4.0,
1443
- "r1": 0.1,
1444
- "r2": 0.2,
1445
- "energy_state": 0.5,
1446
- })
1450
+ # Using initial conditions
1451
+ >>> model.get_args()
1452
+ {"x1": 1.0, "x2": 2.0, "k1": 0.1, "time": 0.0}
1453
+
1454
+ # With custom concentrations
1455
+ >>> model.get_args({"x1": 1.0, "x2": 2.0})
1456
+ {"x1": 1.0, "x2": 2.0, "k1": 0.1, "time": 0.0}
1457
+
1458
+ # With custom concentrations and time
1459
+ >>> model.get_args({"x1": 1.0, "x2": 2.0}, time=1.0)
1460
+ {"x1": 1.0, "x2": 2.0, "k1": 0.1, "time": 1.0}
1447
1461
 
1448
1462
  Args:
1449
- concs (dict[str, float]): A dictionary of concentrations with variable names as keys and their corresponding values as floats.
1450
- time (float, optional): The time point at which to get the concentrations. Default is 0.0.
1451
- include_readouts (bool, optional): Whether to include readout variables in the result. Default is True.
1463
+ concs: A dictionary where keys are the names of the concentrations and values are their respective float values.
1464
+ time: The time point at which the arguments are generated.
1465
+ include_derived: Whether to include derived variables in the arguments.
1466
+ include_readouts: Whether to include readouts in the arguments.
1452
1467
 
1453
1468
  Returns:
1454
- pd.Series: A pandas Series containing the full concentrations for the specified variables.
1469
+ A pandas Series containing the generated arguments with float dtype.
1455
1470
 
1456
1471
  """
1457
- names = self.get_variable_names() + self.get_derived_variable_names()
1472
+ names = self.get_variable_names()
1473
+ if include_derived:
1474
+ names.extend(self.get_derived_variable_names())
1458
1475
  if include_readouts:
1459
- names.extend(self.get_readout_names())
1476
+ names.extend(self._readouts)
1460
1477
 
1461
- return self.get_args(
1462
- concs=concs,
1463
- time=time,
1464
- include_readouts=include_readouts,
1465
- ).loc[names]
1478
+ args = self.get_dependent(
1479
+ concs=concs, time=time, include_readouts=include_readouts
1480
+ )
1481
+ return args.loc[names]
1482
+
1483
+ def get_args_time_course(
1484
+ self,
1485
+ concs: pd.DataFrame,
1486
+ *,
1487
+ include_derived: bool = True,
1488
+ include_readouts: bool = False,
1489
+ ) -> pd.DataFrame:
1490
+ """Generate a DataFrame containing time course arguments for model evaluation.
1491
+
1492
+ Examples:
1493
+ >>> model.get_args_time_course(
1494
+ ... pd.DataFrame({"x1": [1.0, 2.0], "x2": [2.0, 3.0]}
1495
+ ... )
1496
+ pd.DataFrame({
1497
+ "x1": [1.0, 2.0],
1498
+ "x2": [2.0, 3.0],
1499
+ "k1": [0.1, 0.1],
1500
+ "time": [0.0, 1.0]},
1501
+ )
1502
+
1503
+ Args:
1504
+ concs: A DataFrame containing concentration data with time as the index.
1505
+ include_derived: Whether to include derived variables in the arguments.
1506
+ include_readouts: If True, include readout variables in the resulting DataFrame.
1507
+
1508
+ Returns:
1509
+ A DataFrame containing the combined concentration data, parameter values,
1510
+ derived variables, and optionally readout variables, with time as an additional column.
1511
+
1512
+ """
1513
+ names = self.get_variable_names()
1514
+ if include_derived:
1515
+ names.extend(self.get_derived_variable_names())
1516
+
1517
+ args = self.get_dependent_time_course(
1518
+ concs=concs, include_readouts=include_readouts
1519
+ )
1520
+ return args.loc[:, names]
1466
1521
 
1467
1522
  ##########################################################################
1468
1523
  # Get fluxes
@@ -1518,19 +1573,16 @@ class Model:
1518
1573
  Fluxes: A pandas Series containing the fluxes for each reaction.
1519
1574
 
1520
1575
  """
1521
- args = self.get_args(
1576
+ names = self.get_reaction_names()
1577
+ for surrogate in self._surrogates.values():
1578
+ names.extend(surrogate.stoichiometries)
1579
+
1580
+ args = self.get_dependent(
1522
1581
  concs=concs,
1523
1582
  time=time,
1524
1583
  include_readouts=False,
1525
1584
  )
1526
-
1527
- fluxes: dict[str, float] = {}
1528
- for name, rxn in self._reactions.items():
1529
- fluxes[name] = cast(float, rxn.fn(*args.loc[rxn.args]))
1530
-
1531
- for surrogate in self._surrogates.values():
1532
- fluxes |= surrogate.predict(args.loc[surrogate.args].to_numpy())
1533
- return pd.Series(fluxes, dtype=float)
1585
+ return args.loc[names]
1534
1586
 
1535
1587
  def get_fluxes_time_course(self, args: pd.DataFrame) -> pd.DataFrame:
1536
1588
  """Generate a time course of fluxes for the given reactions and surrogates.
@@ -1554,20 +1606,15 @@ class Model:
1554
1606
  the index of the input arguments.
1555
1607
 
1556
1608
  """
1557
- fluxes: dict[str, Float] = {}
1558
- for name, rate in self._reactions.items():
1559
- fluxes[name] = rate.fn(*args.loc[:, rate.args].to_numpy().T)
1560
-
1561
- # Create df here already to avoid having to play around with
1562
- # shape of surrogate outputs
1563
- flux_df = pd.DataFrame(fluxes, index=args.index)
1609
+ names = self.get_reaction_names()
1564
1610
  for surrogate in self._surrogates.values():
1565
- outputs = pd.DataFrame(
1566
- [surrogate.predict(y) for y in args.loc[:, surrogate.args].to_numpy()],
1567
- index=args.index,
1568
- )
1569
- flux_df = pd.concat((flux_df, outputs), axis=1)
1570
- return flux_df
1611
+ names.extend(surrogate.stoichiometries)
1612
+
1613
+ args = self.get_dependent_time_course(
1614
+ concs=args,
1615
+ include_readouts=False,
1616
+ )
1617
+ return args.loc[:, names]
1571
1618
 
1572
1619
  ##########################################################################
1573
1620
  # Get rhs
@@ -1601,22 +1648,21 @@ class Model:
1601
1648
  strict=True,
1602
1649
  )
1603
1650
  )
1604
- args: dict[str, float] = self._get_args(
1651
+ dependent: dict[str, float] = self._get_dependent(
1605
1652
  concs=concsd,
1606
1653
  time=time,
1607
- include_readouts=False,
1654
+ cache=cache,
1608
1655
  )
1609
- fluxes: dict[str, float] = self._get_fluxes(args)
1610
1656
 
1611
1657
  dxdt = cache.dxdt
1612
1658
  dxdt[:] = 0
1613
1659
  for k, stoc in cache.stoich_by_cpds.items():
1614
1660
  for flux, n in stoc.items():
1615
- dxdt[k] += n * fluxes[flux]
1661
+ dxdt[k] += n * dependent[flux]
1616
1662
  for k, sd in cache.dyn_stoich_by_cpds.items():
1617
1663
  for flux, dv in sd.items():
1618
- n = dv.fn(*(args[i] for i in dv.args))
1619
- dxdt[k] += n * fluxes[flux]
1664
+ n = dv.calculate(dependent)
1665
+ dxdt[k] += n * dependent[flux]
1620
1666
  return cast(Array, dxdt.to_numpy())
1621
1667
 
1622
1668
  def get_right_hand_side(
@@ -1650,19 +1696,18 @@ class Model:
1650
1696
  if (cache := self._cache) is None:
1651
1697
  cache = self._create_cache()
1652
1698
  var_names = self.get_variable_names()
1653
- args = self._get_args(
1699
+ dependent = self._get_dependent(
1654
1700
  concs=self.get_initial_conditions() if concs is None else concs,
1655
1701
  time=time,
1656
- include_readouts=False,
1702
+ cache=cache,
1657
1703
  )
1658
- fluxes = self._get_fluxes(args)
1659
1704
  dxdt = pd.Series(np.zeros(len(var_names), dtype=float), index=var_names)
1660
1705
  for k, stoc in cache.stoich_by_cpds.items():
1661
1706
  for flux, n in stoc.items():
1662
- dxdt[k] += n * fluxes[flux]
1707
+ dxdt[k] += n * dependent[flux]
1663
1708
 
1664
1709
  for k, sd in cache.dyn_stoich_by_cpds.items():
1665
1710
  for flux, dv in sd.items():
1666
- n = dv.fn(*(args[i] for i in dv.args))
1667
- dxdt[k] += n * fluxes[flux]
1711
+ n = dv.fn(*(dependent[i] for i in dv.args))
1712
+ dxdt[k] += n * dependent[flux]
1668
1713
  return dxdt
@@ -492,6 +492,16 @@ def _codgen(name: str, sbml: Parser) -> Path:
492
492
  else:
493
493
  variables[k] = v.size
494
494
 
495
+ # Ensure non-zero value for initial assignments
496
+ # EXPLAIN: we need to do this for the first round of get_dependent to work
497
+ # otherwise we run into a ton of DivisionByZero errors.
498
+ # Since the values are overwritte afterwards, it doesn't really matter anyways
499
+ for k in sbml.initial_assignment:
500
+ if k in parameters and parameters[k] == 0:
501
+ parameters[k] = 1
502
+ if k in variables and variables[k] == 0:
503
+ variables[k] = 1
504
+
495
505
  derived_str = "\n ".join(
496
506
  f"m.add_derived('{k}', fn={k}, args={v.args})" for k, v in sbml.derived.items()
497
507
  )
@@ -539,7 +549,7 @@ def get_model() -> Model:
539
549
  {variables_str}
540
550
  {derived_str}
541
551
  {rxn_str}
542
- args = m.get_args()
552
+ args = m.get_dependent()
543
553
  {initial_assignment_source}
544
554
  return m
545
555
  """
modelbase2/scan.py CHANGED
@@ -467,7 +467,7 @@ def steady_state(
467
467
  )
468
468
  concs.index = idx
469
469
  fluxes.index = idx
470
- return SteadyStates(concs, fluxes, parameters=parameters)
470
+ return SteadyStates(concs=concs, fluxes=fluxes, parameters=parameters)
471
471
 
472
472
 
473
473
  def time_course(