equilibrator-pathway-core 0.1.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.
- equilibrator_pathway_core/__init__.py +41 -0
- equilibrator_pathway_core/configuration.py +293 -0
- equilibrator_pathway_core/constants.py +52 -0
- equilibrator_pathway_core/ecm.py +586 -0
- equilibrator_pathway_core/ecm_model.py +205 -0
- equilibrator_pathway_core/errors.py +62 -0
- equilibrator_pathway_core/mdf.py +248 -0
- equilibrator_pathway_core/model.py +709 -0
- equilibrator_pathway_core/solution.py +340 -0
- equilibrator_pathway_core/units.py +224 -0
- equilibrator_pathway_core-0.1.0.dist-info/METADATA +107 -0
- equilibrator_pathway_core-0.1.0.dist-info/RECORD +15 -0
- equilibrator_pathway_core-0.1.0.dist-info/WHEEL +5 -0
- equilibrator_pathway_core-0.1.0.dist-info/licenses/LICENSE +24 -0
- equilibrator_pathway_core-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Max-min driving force and enzyme cost minimization, without a compound database.
|
|
2
|
+
|
|
3
|
+
This is the database-free lower layer of equilibrator-pathway. It analyses
|
|
4
|
+
*fully specified* pathway models -- SBtab files that already carry the
|
|
5
|
+
stoichiometry, bounds, fluxes and standard Gibbs energies -- and never predicts
|
|
6
|
+
or looks anything up. It is shared by equilibrator-pathway (which adds identifier
|
|
7
|
+
resolution, dG'0 prediction and a pint-based API on top) and by the in-browser
|
|
8
|
+
pathway client.
|
|
9
|
+
|
|
10
|
+
Everything here works on plain floats in the canonical units documented in
|
|
11
|
+
:mod:`equilibrator_pathway_core.constants`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from .constants import (
|
|
17
|
+
DEFAULT_RT,
|
|
18
|
+
DEFAULT_T,
|
|
19
|
+
PHYSIOLOGICAL_CONCENTRATION,
|
|
20
|
+
STANDARD_CONCENTRATION,
|
|
21
|
+
R,
|
|
22
|
+
)
|
|
23
|
+
from .ecm import ECF_DEFAULTS, EnzymeCostFunction
|
|
24
|
+
from .errors import ModelError, SolverFailure
|
|
25
|
+
from .mdf import MdfResult, mdf_analysis
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DEFAULT_RT",
|
|
31
|
+
"DEFAULT_T",
|
|
32
|
+
"ECF_DEFAULTS",
|
|
33
|
+
"PHYSIOLOGICAL_CONCENTRATION",
|
|
34
|
+
"STANDARD_CONCENTRATION",
|
|
35
|
+
"EnzymeCostFunction",
|
|
36
|
+
"MdfResult",
|
|
37
|
+
"ModelError",
|
|
38
|
+
"R",
|
|
39
|
+
"SolverFailure",
|
|
40
|
+
"mdf_analysis",
|
|
41
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""The editable Configuration table: its schema, and applying edits to it.
|
|
2
|
+
|
|
3
|
+
The web form that lets the user tweak a model's `Configuration` table is built
|
|
4
|
+
from the schema here rather than from a hand-written list in the JavaScript, so
|
|
5
|
+
that the offered choices cannot drift away from what the optimization actually
|
|
6
|
+
accepts (`ECF_DEFAULTS` and the branches of `EnzymeCostFunction`).
|
|
7
|
+
|
|
8
|
+
Three kinds of options:
|
|
9
|
+
|
|
10
|
+
* `select` -- a fixed set of choices (`algorithm`, `denominator`, ...)
|
|
11
|
+
* `number` -- a numeric parameter (`stdev_factor`, `ln_conc_confidence`, ...)
|
|
12
|
+
* `display` -- shown but not editable: the aqueous conditions, because the ΔG'°
|
|
13
|
+
values stored in the model were already transformed for exactly those
|
|
14
|
+
conditions. Changing the pH here would not re-transform them -- that needs
|
|
15
|
+
Component Contribution, i.e. the compound database this app deliberately does
|
|
16
|
+
without.
|
|
17
|
+
|
|
18
|
+
Moved here from the browser client (``pathway_client/configuration.py``), step 4
|
|
19
|
+
of CORE_EXTRACTION_PLAN.txt: the schema is a capability the library lacked, and
|
|
20
|
+
keeping it next to the code that consumes the options is what stops the two
|
|
21
|
+
drifting apart. The notes are user-facing text, written for the browser's form.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from sbtab.SBtab import SBtabDocument, SBtabTable
|
|
25
|
+
|
|
26
|
+
from .errors import ConfigurationError
|
|
27
|
+
from .model import read_sbtab
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DISPLAY",
|
|
31
|
+
"NUMBER",
|
|
32
|
+
"OPTION_SCHEMA",
|
|
33
|
+
"SELECT",
|
|
34
|
+
"ConfigurationError",
|
|
35
|
+
"apply_overrides",
|
|
36
|
+
"describe_configuration",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
SELECT = "select"
|
|
40
|
+
NUMBER = "number"
|
|
41
|
+
DISPLAY = "display"
|
|
42
|
+
|
|
43
|
+
# Applies to: which algorithm an option has an effect on ("MDF", "ECM" or
|
|
44
|
+
# "both"), so the form can say so.
|
|
45
|
+
OPTION_SCHEMA = {
|
|
46
|
+
"algorithm": {
|
|
47
|
+
"kind": SELECT,
|
|
48
|
+
"choices": ["MDF", "ECM"],
|
|
49
|
+
"label": "Algorithm",
|
|
50
|
+
"applies_to": "both",
|
|
51
|
+
"note": "Which optimization to run. An ECM model also needs a "
|
|
52
|
+
"'Parameter' table with kcat, KM and molecular masses.",
|
|
53
|
+
},
|
|
54
|
+
"version": {
|
|
55
|
+
"kind": SELECT,
|
|
56
|
+
"choices": ["1", "2", "3", "4"],
|
|
57
|
+
"label": "Enzyme cost function level",
|
|
58
|
+
"applies_to": "ECM",
|
|
59
|
+
"note": "1 = capacity only, 2 = + thermodynamics, 3 = + saturation, "
|
|
60
|
+
"4 = + allosteric regulation.",
|
|
61
|
+
},
|
|
62
|
+
"kcat_source": {
|
|
63
|
+
"kind": SELECT,
|
|
64
|
+
"choices": ["gmean", "fwd"],
|
|
65
|
+
"label": "kcat source",
|
|
66
|
+
"applies_to": "ECM",
|
|
67
|
+
"note": "'gmean' distributes the geometric-mean kcat over both "
|
|
68
|
+
"directions using the Haldane relationship; 'fwd' takes kcat in the "
|
|
69
|
+
"direction of the flux as given.",
|
|
70
|
+
},
|
|
71
|
+
"denominator": {
|
|
72
|
+
"kind": SELECT,
|
|
73
|
+
"choices": ["S", "1S", "SP", "1SP", "CM"],
|
|
74
|
+
"label": "Rate law denominator",
|
|
75
|
+
"applies_to": "ECM",
|
|
76
|
+
"note": "The saturation term of the rate law; 'CM' is the common "
|
|
77
|
+
"modular rate law.",
|
|
78
|
+
},
|
|
79
|
+
"regularization": {
|
|
80
|
+
"kind": SELECT,
|
|
81
|
+
"choices": ["volume"],
|
|
82
|
+
"label": "Regularization",
|
|
83
|
+
"applies_to": "ECM",
|
|
84
|
+
"note": "Only 'volume' is implemented: support for the other options "
|
|
85
|
+
"was dropped when equilibrator-pathway moved to CVXPY.",
|
|
86
|
+
},
|
|
87
|
+
"objective": {
|
|
88
|
+
"kind": SELECT,
|
|
89
|
+
"choices": ["enzyme", "enzyme + metabolite"],
|
|
90
|
+
"label": "Objective",
|
|
91
|
+
"applies_to": "ECM",
|
|
92
|
+
"note": "Minimize the enzyme weight alone, or the enzyme plus "
|
|
93
|
+
"metabolite weight.",
|
|
94
|
+
},
|
|
95
|
+
"ln_conc_confidence": {
|
|
96
|
+
"kind": NUMBER,
|
|
97
|
+
"label": "Log-concentration confidence",
|
|
98
|
+
"applies_to": "both",
|
|
99
|
+
"minimum": 0.0,
|
|
100
|
+
"maximum": 0.999999,
|
|
101
|
+
"note": "The confidence level of the concentration bounds (default "
|
|
102
|
+
"0.95). Lowering it narrows the allowed concentration range.",
|
|
103
|
+
},
|
|
104
|
+
"dg_confidence": {
|
|
105
|
+
"kind": NUMBER,
|
|
106
|
+
"label": "ΔG'° confidence",
|
|
107
|
+
"applies_to": "MDF",
|
|
108
|
+
"minimum": 0.0,
|
|
109
|
+
"maximum": 0.999999,
|
|
110
|
+
"note": "Has no effect on an uploaded model: it widens the ΔG'° "
|
|
111
|
+
"bounds using the uncertainty covariance, which a model SBtab does "
|
|
112
|
+
"not store (dg_sigma is None).",
|
|
113
|
+
},
|
|
114
|
+
"stdev_factor": {
|
|
115
|
+
"kind": NUMBER,
|
|
116
|
+
"label": "Standard deviation factor",
|
|
117
|
+
"applies_to": "both",
|
|
118
|
+
"note": "Currently unused by the optimization -- in "
|
|
119
|
+
"equilibrator-pathway it is still a TODO in ecm_model.py, so changing "
|
|
120
|
+
"it will not change the results.",
|
|
121
|
+
},
|
|
122
|
+
"solver": {
|
|
123
|
+
"kind": DISPLAY,
|
|
124
|
+
"label": "Solver",
|
|
125
|
+
"applies_to": "both",
|
|
126
|
+
"note": "CLARABEL is the only solver available in the browser (it is "
|
|
127
|
+
"also eQuilibrator's default).",
|
|
128
|
+
},
|
|
129
|
+
"p_h": {
|
|
130
|
+
"kind": DISPLAY,
|
|
131
|
+
"label": "pH",
|
|
132
|
+
"applies_to": "both",
|
|
133
|
+
"note": "The ΔG'° values in this model were transformed for these "
|
|
134
|
+
"conditions; re-transforming them needs the compound database.",
|
|
135
|
+
},
|
|
136
|
+
"p_mg": {"kind": DISPLAY, "label": "pMg", "applies_to": "both"},
|
|
137
|
+
"ionic_strength": {
|
|
138
|
+
"kind": DISPLAY,
|
|
139
|
+
"label": "Ionic strength",
|
|
140
|
+
"applies_to": "both",
|
|
141
|
+
},
|
|
142
|
+
"temperature": {"kind": DISPLAY, "label": "Temperature", "applies_to": "both"},
|
|
143
|
+
"e_potential": {
|
|
144
|
+
"kind": DISPLAY,
|
|
145
|
+
"label": "Electrostatic potential",
|
|
146
|
+
"applies_to": "both",
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _configuration_table(document: SBtabDocument) -> SBtabTable:
|
|
152
|
+
table = document.get_sbtab_by_id("Configuration")
|
|
153
|
+
if table is None:
|
|
154
|
+
raise ConfigurationError("Missing 'Configuration' table")
|
|
155
|
+
if "Option" not in table.column_names or "Value" not in table.column_names:
|
|
156
|
+
raise ConfigurationError(
|
|
157
|
+
"The Configuration table must have an Option and a Value column"
|
|
158
|
+
)
|
|
159
|
+
return table
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def describe_configuration(text: str, filename: str = "pathway.tsv") -> dict:
|
|
163
|
+
"""Describe the Configuration table of a model, for the edit form.
|
|
164
|
+
|
|
165
|
+
Returns the options in the order they appear in the file, each with its
|
|
166
|
+
current value and how it may be edited. Options that are not in the schema
|
|
167
|
+
are reported as `display`, so an unknown option is shown but never silently
|
|
168
|
+
changed.
|
|
169
|
+
"""
|
|
170
|
+
document = read_sbtab(text, filename)
|
|
171
|
+
table = _configuration_table(document)
|
|
172
|
+
|
|
173
|
+
options = []
|
|
174
|
+
for row in table.to_dict_rows():
|
|
175
|
+
name = row.get("Option", "").strip()
|
|
176
|
+
if not name:
|
|
177
|
+
continue
|
|
178
|
+
value = row.get("Value", "").strip()
|
|
179
|
+
schema = OPTION_SCHEMA.get(name, {})
|
|
180
|
+
option = {
|
|
181
|
+
"name": name,
|
|
182
|
+
"value": value,
|
|
183
|
+
"kind": schema.get("kind", DISPLAY),
|
|
184
|
+
"label": schema.get("label", name),
|
|
185
|
+
"applies_to": schema.get("applies_to", "both"),
|
|
186
|
+
"note": schema.get("note"),
|
|
187
|
+
"comment": (row.get("Comment") or "").strip(),
|
|
188
|
+
}
|
|
189
|
+
if option["kind"] == SELECT:
|
|
190
|
+
choices = list(schema["choices"])
|
|
191
|
+
# keep whatever the file says as a choice, even when it is not one
|
|
192
|
+
# we know about, so that opening the form cannot alter the model
|
|
193
|
+
if not any(choice.lower() == value.lower() for choice in choices):
|
|
194
|
+
if value:
|
|
195
|
+
choices.append(value)
|
|
196
|
+
else:
|
|
197
|
+
# normalize to the schema's spelling of the current value
|
|
198
|
+
option["value"] = next(
|
|
199
|
+
choice for choice in choices if choice.lower() == value.lower()
|
|
200
|
+
)
|
|
201
|
+
option["choices"] = choices
|
|
202
|
+
elif option["kind"] == NUMBER:
|
|
203
|
+
for key in ("minimum", "maximum"):
|
|
204
|
+
if key in schema:
|
|
205
|
+
option[key] = schema[key]
|
|
206
|
+
options.append(option)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
"filename": filename,
|
|
210
|
+
"options": options,
|
|
211
|
+
"algorithm": next(
|
|
212
|
+
(
|
|
213
|
+
option["value"].upper()
|
|
214
|
+
for option in options
|
|
215
|
+
if option["name"] == "algorithm"
|
|
216
|
+
),
|
|
217
|
+
None,
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def apply_overrides(
|
|
223
|
+
document: SBtabDocument, overrides: dict[str, str] | None
|
|
224
|
+
) -> list[str]:
|
|
225
|
+
"""Apply edited Configuration values to a document, in place.
|
|
226
|
+
|
|
227
|
+
Returns a list of human-readable descriptions of what changed, for the
|
|
228
|
+
result page. Raises `ConfigurationError` if an override names an option
|
|
229
|
+
that is absent or not editable, or gives a value the code would not accept.
|
|
230
|
+
"""
|
|
231
|
+
if not overrides:
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
table = _configuration_table(document)
|
|
235
|
+
# the rows are copies, so edits go back through `change_value`, by position
|
|
236
|
+
# (1-based): the Option column need not come first, which rules out
|
|
237
|
+
# `change_value_by_name`
|
|
238
|
+
value_column = table.column_names.index("Value") + 1
|
|
239
|
+
rows_by_name = {}
|
|
240
|
+
for position, row in enumerate(table.to_dict_rows(), start=1):
|
|
241
|
+
name = row.get("Option", "").strip()
|
|
242
|
+
if name:
|
|
243
|
+
rows_by_name.setdefault(name, (position, row))
|
|
244
|
+
|
|
245
|
+
changes = []
|
|
246
|
+
for name, new_value in overrides.items():
|
|
247
|
+
if name not in rows_by_name:
|
|
248
|
+
raise ConfigurationError(
|
|
249
|
+
f"The Configuration table of this model has no option '{name}'"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
position, row = rows_by_name[name]
|
|
253
|
+
schema = OPTION_SCHEMA.get(name, {})
|
|
254
|
+
kind = schema.get("kind", DISPLAY)
|
|
255
|
+
new_value = str(new_value).strip()
|
|
256
|
+
|
|
257
|
+
if kind == DISPLAY:
|
|
258
|
+
raise ConfigurationError(f"The option '{name}' cannot be edited")
|
|
259
|
+
if kind == SELECT:
|
|
260
|
+
choices = schema["choices"]
|
|
261
|
+
match = next(
|
|
262
|
+
(choice for choice in choices if choice.lower() == new_value.lower()),
|
|
263
|
+
None,
|
|
264
|
+
)
|
|
265
|
+
if match is None:
|
|
266
|
+
raise ConfigurationError(
|
|
267
|
+
f"'{new_value}' is not a valid value for '{name}' "
|
|
268
|
+
f"(choose one of: {', '.join(choices)})"
|
|
269
|
+
)
|
|
270
|
+
new_value = match
|
|
271
|
+
elif kind == NUMBER:
|
|
272
|
+
try:
|
|
273
|
+
number = float(new_value)
|
|
274
|
+
except ValueError:
|
|
275
|
+
raise ConfigurationError(
|
|
276
|
+
f"The option '{name}' must be a number, not '{new_value}'"
|
|
277
|
+
)
|
|
278
|
+
minimum, maximum = schema.get("minimum"), schema.get("maximum")
|
|
279
|
+
if minimum is not None and number < minimum:
|
|
280
|
+
raise ConfigurationError(
|
|
281
|
+
f"The option '{name}' must be at least {minimum}"
|
|
282
|
+
)
|
|
283
|
+
if maximum is not None and number > maximum:
|
|
284
|
+
raise ConfigurationError(
|
|
285
|
+
f"The option '{name}' must be at most {maximum}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
old_value = row.get("Value", "").strip()
|
|
289
|
+
if old_value != new_value:
|
|
290
|
+
table.change_value(position, value_column, new_value)
|
|
291
|
+
changes.append(f"{name}: '{old_value}' → '{new_value}'")
|
|
292
|
+
|
|
293
|
+
return changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Physical constants, as plain floats in the core's canonical units.
|
|
2
|
+
|
|
3
|
+
The core does all its arithmetic on unitless numbers in one fixed set of units:
|
|
4
|
+
|
|
5
|
+
concentration M
|
|
6
|
+
energy kJ/mol
|
|
7
|
+
temperature K
|
|
8
|
+
rate constant 1/s
|
|
9
|
+
molecular mass Da
|
|
10
|
+
|
|
11
|
+
Units are attached, if at all, at the boundary: equilibrator-pathway wraps these
|
|
12
|
+
in pint quantities for its public API, and the browser client parses unit
|
|
13
|
+
strings straight into these units. Nothing in here may import pint.
|
|
14
|
+
|
|
15
|
+
These are *defined* here rather than imported from
|
|
16
|
+
``equilibrator_cache.thermodynamic_constants``. Importing that module executes
|
|
17
|
+
``equilibrator_cache/__init__.py``, which pulls in sqlalchemy, pandas, pooch and
|
|
18
|
+
rapidfuzz -- the dependency chain this package exists to avoid. The values are
|
|
19
|
+
held equal to equilibrator-cache's by ``tests/test_constants.py``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_RT",
|
|
26
|
+
"DEFAULT_T",
|
|
27
|
+
"PHYSIOLOGICAL_CONCENTRATION",
|
|
28
|
+
"STANDARD_CONCENTRATION",
|
|
29
|
+
"R",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
#: Gas constant, kJ/(mol*K).
|
|
33
|
+
#:
|
|
34
|
+
#: Note: 8.31e-3, *not* CODATA's 8.314462618e-3. This is the value
|
|
35
|
+
#: equilibrator-cache has always used, and every published eQuilibrator number
|
|
36
|
+
#: -- and every golden result this package is tested against -- was computed
|
|
37
|
+
#: with it. "Correcting" it would move every driving force by ~0.05%, which the
|
|
38
|
+
#: golden tests would (rightly) report as a regression. Change it, if ever, in
|
|
39
|
+
#: equilibrator-cache first and here second.
|
|
40
|
+
R: float = 8.31e-3
|
|
41
|
+
|
|
42
|
+
#: Default temperature, K.
|
|
43
|
+
DEFAULT_T: float = 298.15
|
|
44
|
+
|
|
45
|
+
#: R * DEFAULT_T, kJ/mol.
|
|
46
|
+
DEFAULT_RT: float = R * DEFAULT_T
|
|
47
|
+
|
|
48
|
+
#: Standard-state concentration for a solute, M.
|
|
49
|
+
STANDARD_CONCENTRATION: float = 1.0
|
|
50
|
+
|
|
51
|
+
#: Reference concentration for "physiological" (1 mM) Gibbs energies, M.
|
|
52
|
+
PHYSIOLOGICAL_CONCENTRATION: float = 1.0e-3
|