ode2tn 1.0.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.
- ode2tn/__init__.py +1 -0
- ode2tn/transform.py +384 -0
- ode2tn-1.0.0.dist-info/METADATA +104 -0
- ode2tn-1.0.0.dist-info/RECORD +7 -0
- ode2tn-1.0.0.dist-info/WHEEL +5 -0
- ode2tn-1.0.0.dist-info/licenses/LICENSE +21 -0
- ode2tn-1.0.0.dist-info/top_level.txt +1 -0
ode2tn/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
from ode2tn.transform import *
|
ode2tn/transform.py
ADDED
@@ -0,0 +1,384 @@
|
|
1
|
+
from typing import Any, Iterable
|
2
|
+
import re
|
3
|
+
from typing import Callable
|
4
|
+
|
5
|
+
from scipy.integrate import OdeSolver
|
6
|
+
import gpac as gp
|
7
|
+
import sympy as sp
|
8
|
+
from scipy.integrate._ivp.ivp import OdeResult # noqa
|
9
|
+
|
10
|
+
|
11
|
+
def plot_tn(
|
12
|
+
odes: dict[sp.Symbol | str, sp.Expr | str | float],
|
13
|
+
initial_values: dict[sp.Symbol | str, float],
|
14
|
+
t_eval: Iterable[float] | None = None,
|
15
|
+
*,
|
16
|
+
gamma: float,
|
17
|
+
beta: float,
|
18
|
+
scale: float = 1.0,
|
19
|
+
t_span: tuple[float, float] | None = None,
|
20
|
+
resets: dict[float, dict[sp.Symbol | str, float]] | None = None,
|
21
|
+
dependent_symbols: dict[sp.Symbol | str, sp.Expr | str] | None = None,
|
22
|
+
figure_size: tuple[float, float] = (10, 3),
|
23
|
+
symbols_to_plot: Iterable[sp.Symbol | str] |
|
24
|
+
Iterable[Iterable[sp.Symbol | str]] |
|
25
|
+
str |
|
26
|
+
re.Pattern |
|
27
|
+
Iterable[re.Pattern] |
|
28
|
+
None = None,
|
29
|
+
show: bool = False,
|
30
|
+
method: str | OdeSolver = 'RK45',
|
31
|
+
dense_output: bool = False,
|
32
|
+
events: Callable | Iterable[Callable] | None = None,
|
33
|
+
vectorized: bool = False,
|
34
|
+
return_ode_result: bool = False,
|
35
|
+
args: tuple | None = None,
|
36
|
+
loc: str | tuple[float, float] = 'best',
|
37
|
+
**options,
|
38
|
+
) -> OdeResult | None:
|
39
|
+
"""
|
40
|
+
Plot transcription network (TN) ODEs and initial values.
|
41
|
+
|
42
|
+
For arguments other than odes, initial_values, gamma, and beta, see the documentation for
|
43
|
+
`plot` in the gpac library.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
odes: polynomial ODEs,
|
47
|
+
dict of sp symbols or strings (representing symbols) to sympy expressions or strings or floats
|
48
|
+
(representing RHS of ODEs)
|
49
|
+
Raises ValueError if any of the ODEs RHS is not a polynomial
|
50
|
+
initial_values: initial values,
|
51
|
+
dict of sympy symbols or strings (representing symbols) to floats
|
52
|
+
gamma: coefficient of the negative linear term in the transcription network
|
53
|
+
beta: additive constant in x_top ODE
|
54
|
+
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
55
|
+
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
56
|
+
`x*scale`.
|
57
|
+
resets:
|
58
|
+
If specified, this is a dict mapping times to "configurations"
|
59
|
+
(i.e., dict mapping symbols/str to values).
|
60
|
+
The configurations are used to set the values of the symbols manually during the ODE integration
|
61
|
+
at specific times.
|
62
|
+
Any symbols not appearing as keys in `resets` are left at their current values.
|
63
|
+
The keys can either represent the `x_top` or `x_bot` variables whose ratio represents the original variable
|
64
|
+
(a key in parameter `odes`), or the original variables themselves.
|
65
|
+
If a new `x_top` or `x_bot` variable is used, its value is set directly.
|
66
|
+
If an original variable `x` is used, its then the `x_top` and `x_bot` variables are set
|
67
|
+
as with transforming `inits` to `tn_inits` in `ode2tn`:
|
68
|
+
`x_top` is set to `x*scale`, and `x_bot` is set to `scale`.
|
69
|
+
The OdeResult returned (the one returned by `solve_ivp` in scipy) will have two additional fields:
|
70
|
+
`reset_times` and `reset_indices`, which are lists of the times and indices in `sol.t`
|
71
|
+
corresponding to the times when the resets were applied.
|
72
|
+
Raises a ValueError if any time lies outside the integration interval, or if `resets` is empty,
|
73
|
+
if a symbol is invalid, or if there are symbols representing both an original variable `x` and one of
|
74
|
+
its `x_top` or `x_bot` variables.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
Typically None, but if return_ode_result is True, returns the result of the ODE integration.
|
78
|
+
See documentation of `gpac.plot` for details.
|
79
|
+
"""
|
80
|
+
tn_odes, tn_inits, tn_ratios = ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale)
|
81
|
+
dependent_symbols_tn = dict(dependent_symbols) if dependent_symbols is not None else {}
|
82
|
+
dependent_symbols_tn.update(tn_ratios)
|
83
|
+
symbols_to_plot = dependent_symbols_tn if symbols_to_plot is None else symbols_to_plot
|
84
|
+
|
85
|
+
if resets is not None:
|
86
|
+
# make copy since we are going to change it
|
87
|
+
new_resets = {}
|
88
|
+
for time, reset in resets.items():
|
89
|
+
new_resets[time] = {}
|
90
|
+
for x, val in reset.items():
|
91
|
+
new_resets[time][x] = val
|
92
|
+
resets = new_resets
|
93
|
+
# normalize resets keys and check that variables are valid
|
94
|
+
for reset in resets.values():
|
95
|
+
for x, val in reset.items():
|
96
|
+
if isinstance(x, str):
|
97
|
+
del reset[x]
|
98
|
+
x = sp.symbols(x)
|
99
|
+
reset[x] = val
|
100
|
+
if x not in odes.keys() and x not in tn_odes.keys():
|
101
|
+
raise ValueError(f"Symbol {x} not found in original variables: {', '.join(odes.keys())},\n"
|
102
|
+
f"nor found in transcription network variables: {', '.join(tn_odes.keys())}")
|
103
|
+
# ensure if original variable x is in resets, then neither x_top nor x_bot are in the resets
|
104
|
+
# and substitute x_top and x_bot for x in resets
|
105
|
+
for x, ratio in tn_ratios.items():
|
106
|
+
# x is an original; so make sure neither x_top nor x_bot are in the reset dict
|
107
|
+
if x in reset:
|
108
|
+
xt,xb = sp.fraction(ratio)
|
109
|
+
if xt in reset:
|
110
|
+
raise ValueError(f'Cannot use "top" variable {xt} in resets '
|
111
|
+
f'if original variable {x} is also used')
|
112
|
+
if xb in reset:
|
113
|
+
raise ValueError(f'Cannot use "bottom" variable {xb} in resets '
|
114
|
+
f'if original variable {x} is also used')
|
115
|
+
reset[xt] = reset[x] * scale
|
116
|
+
reset[xb] = scale
|
117
|
+
del reset[x]
|
118
|
+
|
119
|
+
return gp.plot(
|
120
|
+
odes=tn_odes,
|
121
|
+
initial_values=tn_inits,
|
122
|
+
t_eval=t_eval,
|
123
|
+
t_span=t_span,
|
124
|
+
dependent_symbols=dependent_symbols_tn,
|
125
|
+
resets=resets,
|
126
|
+
figure_size=figure_size,
|
127
|
+
symbols_to_plot=symbols_to_plot,
|
128
|
+
show=show,
|
129
|
+
method=method,
|
130
|
+
dense_output=dense_output,
|
131
|
+
events=events,
|
132
|
+
vectorized=vectorized,
|
133
|
+
return_ode_result=return_ode_result,
|
134
|
+
args=args,
|
135
|
+
loc=loc,
|
136
|
+
**options,
|
137
|
+
)
|
138
|
+
|
139
|
+
|
140
|
+
def ode2tn(
|
141
|
+
odes: dict[sp.Symbol | str, sp.Expr | str | float],
|
142
|
+
initial_values: dict[sp.Symbol | str, float],
|
143
|
+
*,
|
144
|
+
gamma: float,
|
145
|
+
beta: float,
|
146
|
+
scale: float = 1.0,
|
147
|
+
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, sp.Expr]]:
|
148
|
+
"""
|
149
|
+
Maps polynomial ODEs and and initial values to transcription network (represented by ODEs with positive
|
150
|
+
Laurent polynomials and negative linear term) simulating it, as well as initial values.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
odes: polynomial ODEs,
|
154
|
+
dict of sympy symbols or strings (representing symbols) to sympy expressions or strings or floats
|
155
|
+
(representing RHS of ODEs)
|
156
|
+
Raises ValueError if any of the ODEs RHS is not a polynomial
|
157
|
+
initial_values: initial values,
|
158
|
+
dict of sympy symbols or strings (representing symbols) to floats
|
159
|
+
gamma: coefficient of the negative linear term in the transcription network
|
160
|
+
beta: additive constant in x_top ODE
|
161
|
+
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
162
|
+
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
163
|
+
`x*scale`.
|
164
|
+
|
165
|
+
Return:
|
166
|
+
triple (`tn_odes`, `tn_inits`, `tn_ratios`), where `tn_ratios` is a dict mapping each original symbol ``x``
|
167
|
+
in the original ODEs to the sympy.Expr ``x_top / x_bot``.
|
168
|
+
"""
|
169
|
+
# normalize initial values dict to use symbols as keys
|
170
|
+
initial_values = {sp.Symbol(symbol) if isinstance(symbol, str) else symbol: value
|
171
|
+
for symbol, value in initial_values.items()}
|
172
|
+
|
173
|
+
# normalize odes dict to use symbols as keys
|
174
|
+
odes_symbols = {}
|
175
|
+
symbols_found_in_expressions = set()
|
176
|
+
for symbol, expr in odes.items():
|
177
|
+
if isinstance(symbol, str):
|
178
|
+
symbol = sp.symbols(symbol)
|
179
|
+
if isinstance(expr, (str, int, float)):
|
180
|
+
expr = sp.sympify(expr)
|
181
|
+
symbols_found_in_expressions.update(expr.free_symbols)
|
182
|
+
odes_symbols[symbol] = expr
|
183
|
+
|
184
|
+
# ensure that all symbols that are keys in `initial_values` are also keys in `odes`
|
185
|
+
initial_values_keys = set(initial_values.keys())
|
186
|
+
odes_keys = set(odes_symbols.keys())
|
187
|
+
diff = initial_values_keys - odes_keys
|
188
|
+
if len(diff) > 0:
|
189
|
+
raise ValueError(f"\nInitial_values contains symbols that are not in odes: "
|
190
|
+
f"{comma_separated(diff)}"
|
191
|
+
f"\nHere are the symbols of the ODES: "
|
192
|
+
f"{comma_separated(odes_keys)}")
|
193
|
+
|
194
|
+
# ensure all symbols in expressions are keys in the odes dict
|
195
|
+
symbols_in_expressions_not_in_odes_keys = symbols_found_in_expressions - odes_keys
|
196
|
+
if len(symbols_in_expressions_not_in_odes_keys) > 0:
|
197
|
+
raise ValueError(f"Found symbols in expressions that are not keys in the odes dict: "
|
198
|
+
f"{symbols_in_expressions_not_in_odes_keys}\n"
|
199
|
+
f"The keys in the odes dict are: {odes_keys}")
|
200
|
+
|
201
|
+
# ensure all odes are polynomials
|
202
|
+
for symbol, expr in odes_symbols.items():
|
203
|
+
if not expr.is_polynomial():
|
204
|
+
raise ValueError(f"ODE for {symbol}' is not a polynomial: {expr}")
|
205
|
+
|
206
|
+
return normalized_ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale)
|
207
|
+
|
208
|
+
|
209
|
+
def normalized_ode2tn(
|
210
|
+
odes: dict[sp.Symbol, sp.Expr],
|
211
|
+
initial_values: dict[sp.Symbol, float],
|
212
|
+
*,
|
213
|
+
gamma: float,
|
214
|
+
beta: float,
|
215
|
+
scale: float,
|
216
|
+
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, sp.Expr]]:
|
217
|
+
# Assumes ode2tn has normalized and done error-checking
|
218
|
+
|
219
|
+
sym2pair: dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]] = {}
|
220
|
+
tn_ratios: dict[sp.Symbol, sp.Expr] = {}
|
221
|
+
for x in odes.keys():
|
222
|
+
# create x_t, x_b for each symbol x
|
223
|
+
x_top, x_bot = sp.symbols(f'{x}_t {x}_b')
|
224
|
+
sym2pair[x] = (x_top, x_bot)
|
225
|
+
tn_ratios[x] = x_top / x_bot
|
226
|
+
|
227
|
+
tn_odes: dict[sp.Symbol, sp.Expr] = {}
|
228
|
+
tn_inits: dict[sp.Symbol, float] = {}
|
229
|
+
for x, expr in odes.items():
|
230
|
+
p_pos, p_neg = split_polynomial(expr)
|
231
|
+
|
232
|
+
# replace sym with sym_top / sym_bot for each original symbol sym
|
233
|
+
for sym in odes.keys():
|
234
|
+
sym_top, sym_bot = sym2pair[sym]
|
235
|
+
p_pos = p_pos.subs(sym, sym_top / sym_bot)
|
236
|
+
p_neg = p_neg.subs(sym, sym_top / sym_bot)
|
237
|
+
|
238
|
+
x_top, x_bot = sym2pair[x]
|
239
|
+
# tn_odes[x_top] = beta + p_pos * x_bot - gamma * x_top
|
240
|
+
# tn_odes[x_bot] = p_neg * x_bot ** 2 / x_top + beta * x_bot / x_top - gamma * x_bot
|
241
|
+
tn_odes[x_top] = beta * x_top / x_bot + p_pos * x_bot - gamma * x_top
|
242
|
+
tn_odes[x_bot] = beta + p_neg * x_bot ** 2 / x_top - gamma * x_bot
|
243
|
+
tn_inits[x_top] = initial_values[x]*scale
|
244
|
+
tn_inits[x_bot] = scale
|
245
|
+
|
246
|
+
return tn_odes, tn_inits, tn_ratios
|
247
|
+
|
248
|
+
|
249
|
+
def split_polynomial(expr: sp.Expr) -> tuple[sp.Expr, sp.Expr]:
|
250
|
+
"""
|
251
|
+
Split a polynomial into two parts:
|
252
|
+
p1: monomials with positive coefficients
|
253
|
+
p2: monomials with negative coefficients (made positive)
|
254
|
+
|
255
|
+
Args:
|
256
|
+
expr: A sympy Expression that is a polynomial
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
pair of sympy Expressions (`p1`, `p2`) such that expr = p1 - p2
|
260
|
+
|
261
|
+
Raises:
|
262
|
+
ValueError: If `expr` is not a polynomial. Note that the constants (sympy type ``Number``)
|
263
|
+
are not considered polynomials by the ``is_polynomial`` method, but we do consider them polynomials
|
264
|
+
and do not raise an exception in this case.
|
265
|
+
"""
|
266
|
+
if expr.is_constant():
|
267
|
+
if expr < 0:
|
268
|
+
return sp.S(0), -expr
|
269
|
+
else:
|
270
|
+
return expr, sp.S(0)
|
271
|
+
|
272
|
+
# Verify it's a polynomial
|
273
|
+
if not expr.is_polynomial():
|
274
|
+
raise ValueError(f"Expression {expr} is not a polynomial")
|
275
|
+
|
276
|
+
# Initialize empty expressions for positive and negative parts
|
277
|
+
p_pos = sp.S(0)
|
278
|
+
p_neg = sp.S(0)
|
279
|
+
|
280
|
+
# Convert to expanded form to make sure all terms are separate
|
281
|
+
expanded = sp.expand(expr)
|
282
|
+
|
283
|
+
# For a sum, we can process each term
|
284
|
+
if expanded.is_Add:
|
285
|
+
for term in expanded.args:
|
286
|
+
# Get the coefficient
|
287
|
+
if term.is_Mul:
|
288
|
+
# For products, find the numeric coefficient
|
289
|
+
coeff = next((arg for arg in term.args if arg.is_number), 1)
|
290
|
+
else:
|
291
|
+
# For non-products (like just x or just a number)
|
292
|
+
coeff = 1 if not term.is_number else term
|
293
|
+
|
294
|
+
# Add to the appropriate part based on sign
|
295
|
+
if coeff > 0:
|
296
|
+
p_pos += term
|
297
|
+
else:
|
298
|
+
# For negative coefficients, add the negated term to p2
|
299
|
+
p_neg += -term
|
300
|
+
elif expanded.is_Mul:
|
301
|
+
# If it's a single term, just check the sign; is_Mul for things like x*y or -x (represented as -1*x)
|
302
|
+
coeff = next((arg for arg in expanded.args if arg.is_number), 1)
|
303
|
+
if coeff > 0:
|
304
|
+
p_pos = expanded
|
305
|
+
else:
|
306
|
+
p_neg = -expanded
|
307
|
+
elif expanded.is_Atom:
|
308
|
+
# since negative terms are technically Mul, i.e., -1*x, if it is an atom then it is positive
|
309
|
+
p_pos = expanded
|
310
|
+
else:
|
311
|
+
# For single constant terms without multiplication, just check the sign;
|
312
|
+
# in tests a term like -x is actually represented as -1*x, so that's covered by the above elif,
|
313
|
+
# but in case of a negative constant like -2, this is handled here
|
314
|
+
if expanded > 0:
|
315
|
+
p_pos = expanded
|
316
|
+
else:
|
317
|
+
p_neg = -expanded
|
318
|
+
|
319
|
+
return p_pos, p_neg
|
320
|
+
|
321
|
+
|
322
|
+
def comma_separated(elts: Iterable[Any]) -> str:
|
323
|
+
return ', '.join(str(elt) for elt in elts)
|
324
|
+
|
325
|
+
|
326
|
+
def main():
|
327
|
+
x_sp, y_sp = gp.species('x y')
|
328
|
+
rxns = [
|
329
|
+
x_sp >> x_sp + y_sp,
|
330
|
+
(3 * y_sp | 2 * y_sp).k(11).r(16.5),
|
331
|
+
(y_sp >> gp.empty).k(6.5),
|
332
|
+
]
|
333
|
+
odes = gp.crn_to_odes(rxns)
|
334
|
+
# extract symbols from odes
|
335
|
+
for var in odes.keys():
|
336
|
+
if var.name == 'x':
|
337
|
+
x = var
|
338
|
+
if var.name == 'y':
|
339
|
+
y = var
|
340
|
+
# for v,ode in odes.items():
|
341
|
+
# print(f"{v}' = {ode}")
|
342
|
+
inits = {
|
343
|
+
x: 1,
|
344
|
+
y: 0.5,
|
345
|
+
}
|
346
|
+
gamma = 20
|
347
|
+
beta = 1
|
348
|
+
scale = 0.1
|
349
|
+
import numpy as np
|
350
|
+
t_eval = np.linspace(0, 20, 500)
|
351
|
+
|
352
|
+
tn_odes, tn_inits, tn_ratios = ode2tn(odes, inits, gamma=gamma, beta=beta, scale=scale)
|
353
|
+
for ratio_symbol in tn_inits.keys():
|
354
|
+
if ratio_symbol.name == 'x_t':
|
355
|
+
xt = ratio_symbol
|
356
|
+
if ratio_symbol.name == 'x_b':
|
357
|
+
xb = ratio_symbol
|
358
|
+
if ratio_symbol.name == 'y_t':
|
359
|
+
yt = ratio_symbol
|
360
|
+
if ratio_symbol.name == 'y_b':
|
361
|
+
yb = ratio_symbol
|
362
|
+
from IPython.display import display
|
363
|
+
for sym, expr in tn_odes.items():
|
364
|
+
print(f"{sym}' = ", end='')
|
365
|
+
display(sp.simplify(expr))
|
366
|
+
print(f'{tn_inits=}')
|
367
|
+
print(f'{tn_ratios=}')
|
368
|
+
# for var, val in tn_inits.items():
|
369
|
+
# tn_inits[var] *= 0.1
|
370
|
+
# print(f'after reducing t/b values by 10x: {tn_inits=}')
|
371
|
+
figsize = (15, 6)
|
372
|
+
resets = {
|
373
|
+
5: {x: 0.5},
|
374
|
+
10: {x: 0.01},
|
375
|
+
15: {x: 0.5},
|
376
|
+
}
|
377
|
+
scale = 0.1
|
378
|
+
plot_tn(odes, inits, gamma=gamma, beta=beta, scale=scale,
|
379
|
+
t_eval=t_eval, resets=resets, figure_size=figsize,
|
380
|
+
symbols_to_plot=[[x, y], [xt, xb, yt, yb]])
|
381
|
+
|
382
|
+
|
383
|
+
if __name__ == '__main__':
|
384
|
+
main()
|
@@ -0,0 +1,104 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: ode2tn
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: A Python package to turn arbitrary polynomial ODEs into a transcriptional network simulating it.
|
5
|
+
Author-email: Dave Doty <doty@ucdavis.edu>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/UC-Davis-molecular-computing/ode-to-transcription-network
|
8
|
+
Project-URL: Issues, https://github.com/UC-Davis-molecular-computing/ode-to-transcription-network/issues
|
9
|
+
Requires-Python: >=3.10
|
10
|
+
Description-Content-Type: text/markdown
|
11
|
+
License-File: LICENSE
|
12
|
+
Requires-Dist: gpac
|
13
|
+
Requires-Dist: scipy>=1.15
|
14
|
+
Requires-Dist: sympy>=1.13
|
15
|
+
Dynamic: license-file
|
16
|
+
|
17
|
+
# ode-to-transcription-network
|
18
|
+
ode2tn is a Python package to turn arbitrary polynomial ODEs into a transcriptional network simulating it.
|
19
|
+
|
20
|
+
See this paper for details: TODO
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Type `pip install ode2tn` at the command line.
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
See the [notebook.ipynb](notebook.ipynb) for more examples of usage.
|
29
|
+
|
30
|
+
The functions `ode2tn` and `plot_tn` are the main elements of the package.
|
31
|
+
`ode2tn` converts a system of arbitrary polynomial ODEs into another system of ODEs representing a transcriptional network as defined in the paper above.
|
32
|
+
Each variable $x$ in the original ODEs is represented by a pair of variables $x^\top,x^\bot$, whose ratio $x^\top / x^\bot$ follows the same dynamics in the transcriptional network as $x$ does in the original ODEs.
|
33
|
+
`plot_tn` does this conversion and then plots the ratios by default, although it can be customized what exactly is plotted;
|
34
|
+
see the documentation for [gpac.plot](https://gpac.readthedocs.io/en/latest/#gpac.ode.plot) for a description of all options.
|
35
|
+
|
36
|
+
Here is a typical way to call each function:
|
37
|
+
|
38
|
+
```python
|
39
|
+
from math import pi
|
40
|
+
import numpy as np
|
41
|
+
import sympy as sp
|
42
|
+
from transform import plot_tn, ode2tn
|
43
|
+
|
44
|
+
x,y = sp.symbols('x y')
|
45
|
+
odes = { # odes dict maps each symbol to an expression for its time derivative
|
46
|
+
x: y-2,
|
47
|
+
y: -x+2,
|
48
|
+
}
|
49
|
+
inits = { # inits maps each symbol to its initial value
|
50
|
+
x: 2,
|
51
|
+
y: 1,
|
52
|
+
}
|
53
|
+
gamma = 2 # uniform decay constant; should be set sufficiently large that ???
|
54
|
+
beta = 1 # constant introduced to keep values from going to infinity or 0
|
55
|
+
t_eval = np.linspace(0, 6*pi, 1000)
|
56
|
+
tn_odes, tn_inits, tn_ratios = ode2tn(odes, inits, gamma, beta)
|
57
|
+
for sym, expr in tn_odes.items():
|
58
|
+
print(f"{sym}' = {expr}")
|
59
|
+
print(f'{tn_inits=}')
|
60
|
+
print(f'{tn_ratios=}')
|
61
|
+
plot_tn(odes, inits, gamma, beta, t_eval=t_eval)
|
62
|
+
```
|
63
|
+
|
64
|
+
This will print
|
65
|
+
|
66
|
+
```
|
67
|
+
x_t' = x_b*y_t/y_b - 2*x_t + x_t/x_b
|
68
|
+
x_b' = 2*x_b**2/x_t - 2*x_b + 1
|
69
|
+
y_t' = 2*y_b - 2*y_t + y_t/y_b
|
70
|
+
y_b' = -2*y_b + 1 + x_t*y_b**2/(x_b*y_t)
|
71
|
+
tn_inits={x_t: 2, x_b: 1, y_t: 1, y_b: 1}
|
72
|
+
tn_ratios={x: x_t/x_b, y: y_t/y_b}
|
73
|
+
```
|
74
|
+
|
75
|
+
showing that the variables `x` and `y` have been replace by pairs `x_t,x_b` and `y_t,y_b`, whose ratios `x_t/x_b` and `y_t/y_b` will track the values of the original variable `x` and `y` over time.
|
76
|
+
The function `plot_tn` above does this conversion and then plots the ratios.
|
77
|
+
Running the code above in a Jupyter notebook will print the above text and show this figure:
|
78
|
+
|
79
|
+

|
80
|
+
|
81
|
+
One could also hand the transcriptional network ODEs to gpac to integrate, if you want to directly access the data being plotted above.
|
82
|
+
The `OdeResult` object returned by `gpac.integrate_odes` is the same returned by [`scipy.integrate.solve_ivp`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html), where the return value `sol` has a field `sol.y` that has the values of the variables in the order they were inserted into `tn_odes`, which will be the same as the order in which the original variables `x` and `y` were inserted, with `x_t` coming before `x_b`:
|
83
|
+
|
84
|
+
```
|
85
|
+
t_eval = np.linspace(0, 2*pi, 5)
|
86
|
+
sol = gp.integrate_odes(tn_odes, tn_inits, t_eval)
|
87
|
+
print(f'times = {sol.t}')
|
88
|
+
print(f'x_y = {sol.y[0]}')
|
89
|
+
print(f'x_b = {sol.y[1]}')
|
90
|
+
print(f'y_t = {sol.y[2]}')
|
91
|
+
print(f'y_b = {sol.y[3]}')
|
92
|
+
```
|
93
|
+
|
94
|
+
which would print
|
95
|
+
|
96
|
+
```
|
97
|
+
times = [0. 1.57079633 3.14159265 4.71238898 6.28318531]
|
98
|
+
x_y = [2. 1.78280757 3.67207594 2.80592514 1.71859172]
|
99
|
+
x_b = [1. 1.78425369 1.83663725 0.93260227 0.859926 ]
|
100
|
+
y_t = [1. 1.87324904 2.14156469 2.10338162 2.74383426]
|
101
|
+
y_b = [1. 0.93637933 0.71348949 1.05261915 2.78279691]
|
102
|
+
```
|
103
|
+
|
104
|
+
|
@@ -0,0 +1,7 @@
|
|
1
|
+
ode2tn/__init__.py,sha256=b_mINIsNfCWzgG7QVYMsRsWKDLvp2QKFAzRqWtYqwDA,30
|
2
|
+
ode2tn/transform.py,sha256=k_yzuUck-RGgS-WI7cUzNRfC4nKIMip_q5mJfVukg28,15847
|
3
|
+
ode2tn-1.0.0.dist-info/licenses/LICENSE,sha256=VV9UH0kkG-2edZvwJOqgtN12bZIzs2vn9_cq1SjoUJc,1091
|
4
|
+
ode2tn-1.0.0.dist-info/METADATA,sha256=UQ7FvLFnOyzWs9qK3fS8NZtsVxd8ESDwT3_QdHpl-cE,4198
|
5
|
+
ode2tn-1.0.0.dist-info/WHEEL,sha256=lTU6B6eIfYoiQJTZNc-fyaR6BpL6ehTzU3xGYxn2n8k,91
|
6
|
+
ode2tn-1.0.0.dist-info/top_level.txt,sha256=fPQ9s5yLIYfazJS7wBBfU9EsWa9RGALq8VL-wUYRlao,7
|
7
|
+
ode2tn-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 UC-Davis Molecular Computing Group
|
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 all
|
13
|
+
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 THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
ode2tn
|