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.
@@ -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