ode2tn 1.0.2__py3-none-any.whl → 1.0.4__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 +8 -1
- ode2tn/transform.py +355 -58
- {ode2tn-1.0.2.dist-info → ode2tn-1.0.4.dist-info}/METADATA +45 -23
- ode2tn-1.0.4.dist-info/RECORD +7 -0
- ode2tn-1.0.2.dist-info/RECORD +0 -7
- {ode2tn-1.0.2.dist-info → ode2tn-1.0.4.dist-info}/WHEEL +0 -0
- {ode2tn-1.0.2.dist-info → ode2tn-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {ode2tn-1.0.2.dist-info → ode2tn-1.0.4.dist-info}/top_level.txt +0 -0
ode2tn/__init__.py
CHANGED
ode2tn/transform.py
CHANGED
@@ -16,8 +16,11 @@ def plot_tn(
|
|
16
16
|
gamma: float,
|
17
17
|
beta: float,
|
18
18
|
scale: float = 1.0,
|
19
|
+
existing_factors: Iterable[sp.Symbol | str] = (),
|
20
|
+
verify_negative_term: bool = True,
|
19
21
|
t_span: tuple[float, float] | None = None,
|
20
22
|
show_factors: bool = False,
|
23
|
+
latex_legend: bool = True,
|
21
24
|
resets: dict[float, dict[sp.Symbol | str, float]] | None = None,
|
22
25
|
dependent_symbols: dict[sp.Symbol | str, sp.Expr | str] | None = None,
|
23
26
|
figure_size: tuple[float, float] = (10, 3),
|
@@ -48,19 +51,32 @@ def plot_tn(
|
|
48
51
|
dict of sp symbols or strings (representing symbols) to sympy expressions or strings or floats
|
49
52
|
(representing RHS of ODEs)
|
50
53
|
Raises ValueError if any of the ODEs RHS is not a polynomial
|
54
|
+
|
51
55
|
initial_values: initial values,
|
52
56
|
dict of sympy symbols or strings (representing symbols) to floats
|
57
|
+
|
53
58
|
gamma: coefficient of the negative linear term in the transcription network
|
59
|
+
|
54
60
|
beta: additive constant in x_top ODE
|
61
|
+
|
55
62
|
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
56
63
|
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
57
64
|
`x*scale`.
|
65
|
+
|
58
66
|
show_factors: if True, then in addition to plotting the ratios x_top/x_bot,
|
59
67
|
also plot the factors x_top and x_bot separately in a second plot.
|
60
68
|
Mutually exlusive with `symbols_to_plot`, since it is equivalent to setting
|
61
69
|
`symbols_to_plot` to ``[ratios, factors]``, where ratios is a list of dependent symbols
|
62
70
|
`x=x_top/x_bot`, and factors is a list of symbols with the transcription factors `x_top`, `x_bot`,
|
63
71
|
for each original variable `x`.
|
72
|
+
|
73
|
+
latex_legend: If True, surround each symbol name with dollar signs, unless it is already surrounded with them,
|
74
|
+
so that the legend is interpreted as LaTeX. If this is True, then the symbol name must either
|
75
|
+
start and end with `$`, or neither start nor end with `$`. Unlike in the gpac package, this is True
|
76
|
+
by default. The names of transcription factors are automatically surrounded by dollar signs.
|
77
|
+
This option makes sure the legend showing original variables (or dependent symbols) also have `$` added
|
78
|
+
so as to be interpreted as LaTeX.
|
79
|
+
|
64
80
|
resets:
|
65
81
|
If specified, this is a dict mapping times to "configurations"
|
66
82
|
(i.e., dict mapping symbols/str to values).
|
@@ -80,6 +96,26 @@ def plot_tn(
|
|
80
96
|
if a symbol is invalid, or if there are symbols representing both an original variable `x` and one of
|
81
97
|
its `x_top` or `x_bot` variables.
|
82
98
|
|
99
|
+
existing_factors: iterable of symbols (or strings with names of symbols) to "pass through" unchanged.
|
100
|
+
These symbols are not transformed into `x_top` and `x_bot` variables, but are interpreted as
|
101
|
+
being transcription factors already. Raises exception if any of these symbols are
|
102
|
+
not in the original ODEs. Also raises exception if any have an ode not of the form
|
103
|
+
required as a transcriptional network: a Laurent polynomial in the variables with a single negative
|
104
|
+
term `-gamma * x`, where `x` is the transription factor.
|
105
|
+
|
106
|
+
verify_negative_term: If False, then do not check that the existing factors
|
107
|
+
have an ODE with a single negative term of the form `-gamma*x`. The default is True, but this can lead to
|
108
|
+
false positives. If one constructs a sympy expression such as `expr- gamma*x`, it could be that expr
|
109
|
+
can be simplyfied to `alpha*x` for some constant `alpha` > `gamma`. Then the expression would be
|
110
|
+
simplified to `(alpha-gamma)*x`, so would no longer have a negative term (or if `alpha < gamma`, it would
|
111
|
+
have a negative term, but with the wrong coefficient `(alpha-gamma)` instead of `gamma`). However,
|
112
|
+
since one writing an ODE by hand would tend to write it already in simplified form (e.g.,
|
113
|
+
it would be awkward to write `odes = {x: 1.7*x - 0.5*x}` rather than simply `odes = {x: 1.2*x}`),
|
114
|
+
we leave the check on by default. Only turn off this check if you are confident that the
|
115
|
+
ODEs already contain a negative term of the form `-gamma*x`.
|
116
|
+
It is still verified that the ODE is a Laurent polynomial; since this condition is independent of
|
117
|
+
the form of the expression.
|
118
|
+
|
83
119
|
Returns:
|
84
120
|
Typically None, but if return_ode_result is True, returns the result of the ODE integration.
|
85
121
|
See documentation of `gpac.plot` for details.
|
@@ -87,7 +123,9 @@ def plot_tn(
|
|
87
123
|
if show_factors and symbols_to_plot is not None:
|
88
124
|
raise ValueError("Cannot use both show_factors and symbols_to_plot at the same time.")
|
89
125
|
|
90
|
-
tn_odes, tn_inits, tn_syms = ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale
|
126
|
+
tn_odes, tn_inits, tn_syms = ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale,
|
127
|
+
existing_factors=existing_factors,
|
128
|
+
verify_negative_term=verify_negative_term)
|
91
129
|
dependent_symbols_tn = dict(dependent_symbols) if dependent_symbols is not None else {}
|
92
130
|
tn_ratios = {sym: sym_t/sym_b for sym, (sym_t, sym_b) in tn_syms.items()}
|
93
131
|
dependent_symbols_tn.update(tn_ratios)
|
@@ -112,6 +150,7 @@ def plot_tn(
|
|
112
150
|
dependent_symbols=dependent_symbols_tn,
|
113
151
|
resets=resets,
|
114
152
|
figure_size=figure_size,
|
153
|
+
latex_legend=latex_legend,
|
115
154
|
symbols_to_plot=symbols_to_plot,
|
116
155
|
legend=legend,
|
117
156
|
show=show,
|
@@ -170,6 +209,9 @@ def ode2tn(
|
|
170
209
|
gamma: float,
|
171
210
|
beta: float,
|
172
211
|
scale: float = 1.0,
|
212
|
+
ignore: Iterable[sp.Symbol] = (),
|
213
|
+
existing_factors: Iterable[sp.Symbol | str] = (),
|
214
|
+
verify_negative_term: bool = True,
|
173
215
|
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]]]:
|
174
216
|
"""
|
175
217
|
Maps polynomial ODEs and and initial values to transcription network (represented by ODEs with positive
|
@@ -180,15 +222,43 @@ def ode2tn(
|
|
180
222
|
dict of sympy symbols or strings (representing symbols) to sympy expressions or strings or floats
|
181
223
|
(representing RHS of ODEs)
|
182
224
|
Raises ValueError if any of the ODEs RHS is not a polynomial
|
225
|
+
|
183
226
|
initial_values: initial values,
|
184
227
|
dict of sympy symbols or strings (representing symbols) or gpac.Specie (representing chemical
|
185
228
|
species, if the ODEs were derived from `gpac.crn_to_odes`) to floats
|
229
|
+
|
186
230
|
gamma: coefficient of the negative linear term in the transcription network
|
231
|
+
|
187
232
|
beta: additive constant in x_top ODE
|
233
|
+
|
188
234
|
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
189
235
|
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
190
236
|
`x*scale`.
|
191
237
|
|
238
|
+
ignore: variables to ignore and not transform into `x_top` and `x_bot` variables. Note this is different
|
239
|
+
from `existing_factors`, in the sense that variables in `ignore` are not put into the output
|
240
|
+
ODEs.
|
241
|
+
|
242
|
+
existing_factors: iterable of symbols (or strings with names of symbols) to "pass through" unchanged.
|
243
|
+
These symbols are not transformed into `x_top` and `x_bot` variables, but are interpreted as
|
244
|
+
being transcription factors already. Raises exception if any of these symbols are
|
245
|
+
not in the original ODEs. Also raises exception if any have an ode not of the form
|
246
|
+
required as a transcriptional network: a Laurent polynomial in the variables with a single negative
|
247
|
+
term `-gamma * x`, where `x` is the transription factor.
|
248
|
+
|
249
|
+
verify_negative_term: If False, then do not check that the existing factors
|
250
|
+
have an ODE with a single negative term of the form `-gamma*x`. The default is True, but this can lead to
|
251
|
+
false positives. If one constructs a sympy expression such as `expr- gamma*x`, it could be that expr
|
252
|
+
can be simplyfied to `alpha*x` for some constant `alpha` > `gamma`. Then the expression would be
|
253
|
+
simplified to `(alpha-gamma)*x`, so would no longer have a negative term (or if `alpha < gamma`, it would
|
254
|
+
have a negative term, but with the wrong coefficient `(alpha-gamma)` instead of `gamma`). However,
|
255
|
+
since one writing an ODE by hand would tend to write it already in simplified form (e.g.,
|
256
|
+
it would be awkward to write `odes = {x: 1.7*x - 0.5*x}` rather than simply `odes = {x: 1.2*x}`),
|
257
|
+
we leave the check on by default. Only turn off this check if you are confident that the
|
258
|
+
ODEs already contain a negative term of the form `-gamma*x`.
|
259
|
+
It is still verified that the ODE is a Laurent polynomial; since this condition is independent of
|
260
|
+
the form of the expression.
|
261
|
+
|
192
262
|
Return:
|
193
263
|
triple (`tn_odes`, `tn_inits`, `tn_syms`), where `tn_syms` is a dict mapping each original symbol ``x``
|
194
264
|
in the original ODEs to the pair ``(x_top, x_bot)``.
|
@@ -203,6 +273,10 @@ def ode2tn(
|
|
203
273
|
initial_values_norm[symbol] = value
|
204
274
|
initial_values = initial_values_norm
|
205
275
|
|
276
|
+
# normalize existing_factors to be symbols
|
277
|
+
existing_factors: list[sp.Symbol] = [sp.Symbol(factor) if isinstance(factor, str) else factor
|
278
|
+
for factor in existing_factors]
|
279
|
+
|
206
280
|
# normalize odes dict to use symbols as keys
|
207
281
|
odes_normalized = {}
|
208
282
|
symbols_found_in_expressions = set()
|
@@ -237,8 +311,156 @@ def ode2tn(
|
|
237
311
|
if not expr.is_polynomial():
|
238
312
|
raise ValueError(f"ODE for {symbol}' is not a polynomial: {expr}")
|
239
313
|
|
240
|
-
return normalized_ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale)
|
314
|
+
return normalized_ode2tn(odes, initial_values, gamma=gamma, beta=beta, scale=scale, ignore=list(ignore),
|
315
|
+
existing_factors=existing_factors, verify_negative_term=verify_negative_term)
|
316
|
+
|
317
|
+
|
318
|
+
def check_x_is_transcription_factor(x: sp.Symbol, ode: sp.Expr, gamma: float, verify_negative_term: bool) -> None:
|
319
|
+
"""
|
320
|
+
Check if 'ode' is a Laurent polynomial with a single negative term -gamma*x.
|
321
|
+
|
322
|
+
Parameters
|
323
|
+
----------
|
324
|
+
x
|
325
|
+
The symbol that should appear in the negative term.
|
326
|
+
|
327
|
+
ode
|
328
|
+
The symbolic expression to check.
|
241
329
|
|
330
|
+
gamma
|
331
|
+
The expected coefficient of the negative term.
|
332
|
+
|
333
|
+
verify_negative_term
|
334
|
+
Whether to check for the negative term -gamma*x.
|
335
|
+
|
336
|
+
Raises
|
337
|
+
------
|
338
|
+
ValueError
|
339
|
+
If 'ode' is not a Laurent polynomial or if it doesn't have exactly
|
340
|
+
one negative term of the form -gamma*x.
|
341
|
+
"""
|
342
|
+
# Expand the expression to get it in a standard form
|
343
|
+
expanded_ode = sp.expand(ode)
|
344
|
+
|
345
|
+
# Check if the expression is a Laurent polynomial
|
346
|
+
if not is_laurent_polynomial(expanded_ode):
|
347
|
+
raise ValueError(f"The expression `{ode}` is not a Laurent polynomial")
|
348
|
+
|
349
|
+
if not verify_negative_term:
|
350
|
+
return
|
351
|
+
|
352
|
+
# Collect terms and check for the negative term -gamma*x
|
353
|
+
terms = expanded_ode.as_ordered_terms()
|
354
|
+
|
355
|
+
# Find negative terms that contain x
|
356
|
+
negative_terms = [term for term in terms if is_negative(term) and x in term.free_symbols]
|
357
|
+
|
358
|
+
# Check if there's exactly one negative term
|
359
|
+
if len(negative_terms) != 1:
|
360
|
+
raise ValueError(f"Expected exactly one negative term with {x}, found {len(negative_terms)} in `{ode}`")
|
361
|
+
|
362
|
+
# Check if the negative term has the form -gamma*x
|
363
|
+
negative_term = negative_terms[0]
|
364
|
+
|
365
|
+
# Try to extract the coefficient of x
|
366
|
+
coeff = sp.Wild('coeff')
|
367
|
+
match_dict = negative_term.match(-coeff * x)
|
368
|
+
|
369
|
+
if match_dict is None:
|
370
|
+
raise ValueError(f"The negative term {negative_term} doesn't have the form -gamma*{x} in expression {ode}")
|
371
|
+
|
372
|
+
actual_gamma = float(match_dict[coeff])
|
373
|
+
|
374
|
+
# Check if the coefficient is approximately equal to gamma
|
375
|
+
if not is_approximately_equal(actual_gamma, gamma):
|
376
|
+
raise ValueError(f"Expected coefficient {gamma}, got {actual_gamma} in expression {ode}")
|
377
|
+
|
378
|
+
|
379
|
+
def is_laurent_polynomial(expr: sp.Expr) -> bool:
|
380
|
+
"""
|
381
|
+
Check if an expression is a Laurent polynomial (polynomial with possible negative exponents).
|
382
|
+
|
383
|
+
Parameters
|
384
|
+
----------
|
385
|
+
expr
|
386
|
+
The symbolic expression to check.
|
387
|
+
|
388
|
+
Returns
|
389
|
+
-------
|
390
|
+
:
|
391
|
+
True if the expression is a Laurent polynomial, False otherwise.
|
392
|
+
"""
|
393
|
+
if expr.is_Add:
|
394
|
+
return all(is_laurent_polynomial(term) for term in expr.args)
|
395
|
+
|
396
|
+
if expr.is_Mul:
|
397
|
+
return all(is_laurent_monomial(factor) for factor in expr.args)
|
398
|
+
|
399
|
+
# Constants are Laurent polynomials
|
400
|
+
if expr.is_number:
|
401
|
+
return True
|
402
|
+
|
403
|
+
# Single symbols are Laurent polynomials
|
404
|
+
if expr.is_Symbol:
|
405
|
+
return True
|
406
|
+
|
407
|
+
return is_laurent_monomial(expr)
|
408
|
+
|
409
|
+
|
410
|
+
def is_laurent_monomial(expr: sp.Expr) -> bool:
|
411
|
+
"""
|
412
|
+
Check if an expression is a Laurent monomial (term with possible negative exponents).
|
413
|
+
|
414
|
+
Parameters
|
415
|
+
----------
|
416
|
+
expr
|
417
|
+
The symbolic expression to check.
|
418
|
+
|
419
|
+
Returns
|
420
|
+
-------
|
421
|
+
:
|
422
|
+
True if the expression is a Laurent monomial, False otherwise.
|
423
|
+
"""
|
424
|
+
# Handle division expressions (a/b) by rewriting as a*b^(-1)
|
425
|
+
if expr.is_Mul and any(arg.is_Pow and arg.args[1] < 0 for arg in expr.args):
|
426
|
+
return all(factor.is_Symbol or
|
427
|
+
(factor.is_Pow and factor.args[0].is_Symbol and factor.args[1].is_Integer) or
|
428
|
+
factor.is_number for factor in expr.args)
|
429
|
+
|
430
|
+
# Direct division (x/y) is rewritten to x*y^(-1) by SymPy
|
431
|
+
# But we'll handle it explicitly in case it's encountered directly
|
432
|
+
if expr.is_Pow:
|
433
|
+
base, exp = expr.args
|
434
|
+
return base.is_Symbol and exp.is_Integer
|
435
|
+
|
436
|
+
# Handle direct division representation
|
437
|
+
if hasattr(expr, 'func') and expr.func == sp.core.mul.Mul and len(expr.args) == 2:
|
438
|
+
if hasattr(expr, 'is_commutative') and expr.is_commutative:
|
439
|
+
numer, denom = expr.as_numer_denom()
|
440
|
+
return numer.is_Symbol and denom.is_Symbol
|
441
|
+
|
442
|
+
return expr.is_Symbol or expr.is_number
|
443
|
+
|
444
|
+
|
445
|
+
def is_approximately_equal(a: float, b: float, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
|
446
|
+
"""
|
447
|
+
Check if two numbers are approximately equal within specified tolerances.
|
448
|
+
|
449
|
+
Parameters
|
450
|
+
----------
|
451
|
+
a, b
|
452
|
+
The numbers to compare.
|
453
|
+
rtol
|
454
|
+
The relative tolerance.
|
455
|
+
atol
|
456
|
+
The absolute tolerance.
|
457
|
+
|
458
|
+
Returns
|
459
|
+
-------
|
460
|
+
:
|
461
|
+
True if the numbers are approximately equal, False otherwise.
|
462
|
+
"""
|
463
|
+
return abs(a - b) <= (atol + rtol * abs(b))
|
242
464
|
|
243
465
|
def normalized_ode2tn(
|
244
466
|
odes: dict[sp.Symbol, sp.Expr],
|
@@ -247,34 +469,51 @@ def normalized_ode2tn(
|
|
247
469
|
gamma: float,
|
248
470
|
beta: float,
|
249
471
|
scale: float,
|
472
|
+
ignore: list[sp.Symbol],
|
473
|
+
existing_factors: list[sp.Symbol],
|
474
|
+
verify_negative_term: bool,
|
250
475
|
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]]]:
|
251
476
|
# Assumes ode2tn has normalized and done error-checking
|
252
477
|
|
253
478
|
tn_syms: dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]] = {}
|
254
479
|
for x in odes.keys():
|
255
|
-
|
256
|
-
|
257
|
-
|
480
|
+
if x not in existing_factors and x not in ignore:
|
481
|
+
# create x_t, x_b for each symbol x
|
482
|
+
x_t, x_b = sp.symbols(f'{x}_t {x}_b')
|
483
|
+
tn_syms[x] = (x_t, x_b)
|
258
484
|
|
259
485
|
tn_odes: dict[sp.Symbol, sp.Expr] = {}
|
260
486
|
tn_inits: dict[sp.Symbol, float] = {}
|
261
|
-
for x,
|
262
|
-
|
487
|
+
for x, ode in odes.items():
|
488
|
+
if x in ignore:
|
489
|
+
continue
|
490
|
+
if x in existing_factors:
|
491
|
+
check_x_is_transcription_factor(x, odes[x], gamma=gamma, verify_negative_term=verify_negative_term)
|
492
|
+
ode = odes[x]
|
493
|
+
for y, (y_t, y_b) in tn_syms.items():
|
494
|
+
ratio = y_t / y_b
|
495
|
+
ode = ode.subs(y, ratio)
|
496
|
+
tn_odes[x] = ode
|
497
|
+
tn_inits[x] = initial_values.get(x, 0) * scale
|
498
|
+
continue
|
499
|
+
p_pos, p_neg = split_polynomial(ode)
|
263
500
|
|
264
501
|
# replace sym with sym_top / sym_bot for each original symbol sym
|
265
502
|
for sym in odes.keys():
|
503
|
+
if sym in existing_factors or sym in ignore:
|
504
|
+
continue
|
266
505
|
sym_top, sym_bot = tn_syms[sym]
|
267
506
|
ratio = sym_top / sym_bot
|
268
507
|
p_pos = p_pos.subs(sym, ratio)
|
269
508
|
p_neg = p_neg.subs(sym, ratio)
|
270
509
|
|
271
510
|
x_t, x_b = tn_syms[x]
|
272
|
-
# tn_odes[x_top] = beta + p_pos * x_bot - gamma * x_top
|
273
|
-
# tn_odes[x_bot] = p_neg * x_bot ** 2 / x_top + beta * x_bot / x_top - gamma * x_bot
|
274
511
|
tn_odes[x_t] = beta * x_t / x_b + p_pos * x_b - gamma * x_t
|
275
512
|
tn_odes[x_b] = beta + p_neg * x_b ** 2 / x_t - gamma * x_b
|
276
513
|
tn_inits[x_t] = initial_values.get(x, 0) * scale
|
277
514
|
tn_inits[x_b] = scale
|
515
|
+
check_x_is_transcription_factor(x_t, tn_odes[x_t], gamma=gamma, verify_negative_term=False)
|
516
|
+
check_x_is_transcription_factor(x_b, tn_odes[x_b], gamma=gamma, verify_negative_term=False)
|
278
517
|
|
279
518
|
return tn_odes, tn_inits, tn_syms
|
280
519
|
|
@@ -285,22 +524,27 @@ def split_polynomial(expr: sp.Expr) -> tuple[sp.Expr, sp.Expr]:
|
|
285
524
|
p1: monomials with positive coefficients
|
286
525
|
p2: monomials with negative coefficients (made positive)
|
287
526
|
|
288
|
-
|
289
|
-
|
527
|
+
Parameters
|
528
|
+
----------
|
529
|
+
expr: A sympy Expression that is a polynomial
|
290
530
|
|
291
|
-
Returns
|
531
|
+
Returns
|
532
|
+
-------
|
533
|
+
:
|
292
534
|
pair of sympy Expressions (`p1`, `p2`) such that expr = p1 - p2
|
293
535
|
|
294
|
-
Raises
|
295
|
-
|
536
|
+
Raises
|
537
|
+
------
|
538
|
+
ValueError:
|
539
|
+
If `expr` is not a polynomial. Note that the constants (sympy type ``Number``)
|
296
540
|
are not considered polynomials by the ``is_polynomial`` method, but we do consider them polynomials
|
297
541
|
and do not raise an exception in this case.
|
298
542
|
"""
|
299
|
-
if expr.is_constant():
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
543
|
+
# if expr.is_constant():
|
544
|
+
# if expr < 0:
|
545
|
+
# return sp.S(0), -expr
|
546
|
+
# else:
|
547
|
+
# return expr, sp.S(0)
|
304
548
|
|
305
549
|
# Verify it's a polynomial
|
306
550
|
if not expr.is_polynomial():
|
@@ -316,38 +560,15 @@ def split_polynomial(expr: sp.Expr) -> tuple[sp.Expr, sp.Expr]:
|
|
316
560
|
# For a sum, we can process each term
|
317
561
|
if expanded.is_Add:
|
318
562
|
for term in expanded.args:
|
319
|
-
|
320
|
-
|
321
|
-
# For products, find the numeric coefficient
|
322
|
-
coeff = next((arg for arg in term.args if arg.is_number), 1)
|
563
|
+
if is_negative(term):
|
564
|
+
p_neg += -term
|
323
565
|
else:
|
324
|
-
# For non-products (like just x or just a number)
|
325
|
-
coeff = 1 if not term.is_number else term
|
326
|
-
|
327
|
-
# Add to the appropriate part based on sign
|
328
|
-
if coeff > 0:
|
329
566
|
p_pos += term
|
330
|
-
else:
|
331
|
-
# For negative coefficients, add the negated term to p2
|
332
|
-
p_neg += -term
|
333
|
-
elif expanded.is_Mul or expanded.is_Pow:
|
334
|
-
# If it's a single term, just check the sign; is_Mul for things like x*y or -x (represented as -1*x)
|
335
|
-
coeff = next((arg for arg in expanded.args if arg.is_number), 1)
|
336
|
-
if coeff > 0:
|
337
|
-
p_pos = expanded
|
338
|
-
else:
|
339
|
-
p_neg = -expanded
|
340
|
-
elif expanded.is_Atom:
|
341
|
-
# since negative terms are technically Mul, i.e., -1*x, if it is an atom then it is positive
|
342
|
-
p_pos = expanded
|
343
567
|
else:
|
344
|
-
|
345
|
-
# in tests a term like -x is actually represented as -1*x, so that's covered by the above elif,
|
346
|
-
# but in case of a negative constant like -2, this is handled here
|
347
|
-
if expanded > 0:
|
348
|
-
p_pos = expanded
|
349
|
-
else:
|
568
|
+
if is_negative(expanded):
|
350
569
|
p_neg = -expanded
|
570
|
+
else:
|
571
|
+
p_pos = expanded
|
351
572
|
|
352
573
|
return p_pos, p_neg
|
353
574
|
|
@@ -356,23 +577,99 @@ def comma_separated(elts: Iterable[Any]) -> str:
|
|
356
577
|
return ', '.join(str(elt) for elt in elts)
|
357
578
|
|
358
579
|
|
580
|
+
def is_negative(expr: sp.Symbol | sp.Expr | float | int) -> bool:
|
581
|
+
if isinstance(expr, (float, int, sp.Number)):
|
582
|
+
return expr < 0
|
583
|
+
elif expr.is_Atom or expr.is_Pow:
|
584
|
+
return False
|
585
|
+
elif expr.is_Add:
|
586
|
+
raise ValueError(f"Expression {expr} is not a single term")
|
587
|
+
elif expr.is_Mul:
|
588
|
+
arg0 = expr.args[0]
|
589
|
+
if isinstance(arg0, sp.Symbol):
|
590
|
+
return False
|
591
|
+
return arg0.is_negative
|
592
|
+
else:
|
593
|
+
raise ValueError(f"Unrecognized type {type(expr)} of expression `{expr}`")
|
594
|
+
|
359
595
|
def main():
|
596
|
+
from sympy.abc import p,q,w,z,x
|
597
|
+
|
360
598
|
import gpac as gp
|
361
599
|
import numpy as np
|
362
600
|
import sympy as sp
|
363
|
-
from ode2tn import
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
odes = gp.crn_to_odes(rxns)
|
368
|
-
inits = {x: 1}
|
369
|
-
t_eval = np.linspace(0, 1, 100)
|
370
|
-
gamma = 1
|
601
|
+
from ode2tn import ode2tn
|
602
|
+
import matplotlib.pyplot as plt
|
603
|
+
|
604
|
+
gamma = 20
|
371
605
|
beta = 1
|
372
|
-
#
|
606
|
+
shift = 2 # amount by which to shift oscillator up to maintain positivity
|
607
|
+
x, p, q, w, z, zap = sp.symbols('x p q w z zap')
|
608
|
+
# objective function f as a sympy expression
|
609
|
+
f_exp = sp.exp(-(x - 3) ** 2 / 0.5) + 1.5 * sp.exp(-(x - 5) ** 2 / 1.5)
|
610
|
+
f_plot = sp.plot(f_exp, (x, 0, 8), size=(6, 2))
|
611
|
+
|
612
|
+
# next line commented out because it generates a new plot for some reason; uncomment to save the plot to a file
|
613
|
+
# f_plot.save("extremum-seek-objective-function.pdf")
|
614
|
+
|
615
|
+
# f as a Python function that can be called with sympy Expression inputs to substitute for the variable x
|
616
|
+
def f(expr):
|
617
|
+
return f_exp.subs(x, expr)
|
618
|
+
|
619
|
+
omega = 3 # frequency of oscillation
|
620
|
+
lmbda = 0.1 * omega
|
621
|
+
k = 0.5 * lmbda # rate of convergence
|
622
|
+
a = 0.1 # magnitude of oscillation
|
623
|
+
odes = {
|
624
|
+
p: omega * (q - shift),
|
625
|
+
q: -omega * (p - shift),
|
626
|
+
w: -lmbda * (w - shift), # + f(x)*(p-shift), # we add this after doing the construction
|
627
|
+
z: k * (w - shift),
|
628
|
+
x: gamma * (z + a * p - x),
|
629
|
+
}
|
630
|
+
inits = {
|
631
|
+
p: 0 + shift,
|
632
|
+
q: 1 + shift,
|
633
|
+
w: 0 + shift,
|
634
|
+
z: 2, # z(0) sets initial point of search
|
635
|
+
}
|
636
|
+
t_eval = np.linspace(0, 300, 1000)
|
637
|
+
|
638
|
+
# we manually plot instead of calling gpac.plot in order to show values of x for different initial values of z in one plot
|
639
|
+
|
640
|
+
tn_odes, tn_inits, tn_syms = ode2tn(odes, inits, gamma=gamma, beta=beta, existing_factors=[x])
|
641
|
+
wt, wb = tn_syms[w]
|
642
|
+
pt, pb = tn_syms[p]
|
643
|
+
zt, zb = tn_syms[z]
|
644
|
+
tn_odes[wt] += f(x) * (pt / pb - shift) * wb
|
645
|
+
|
646
|
+
x_idx = 0
|
647
|
+
found = False
|
648
|
+
for var in tn_odes.keys():
|
649
|
+
if var == x:
|
650
|
+
found = True
|
651
|
+
break
|
652
|
+
x_idx += 1
|
653
|
+
assert found
|
654
|
+
|
655
|
+
plt.figure(figsize=(12, 5))
|
656
|
+
|
657
|
+
for z_init in np.arange(2, 7.5, 0.5):
|
658
|
+
tn_inits[zt] = z_init
|
659
|
+
tn_inits[zb] = 1
|
660
|
+
sol = gp.integrate_odes(tn_odes, tn_inits, t_eval)
|
661
|
+
x_vals = sol.y[x_idx]
|
662
|
+
label = f"x for z(0)={z_init}"
|
663
|
+
times = sol.t
|
664
|
+
plt.plot(times, x_vals, label=label, color="blue")
|
665
|
+
|
666
|
+
plt.yticks(range(8))
|
667
|
+
plt.ylim(1, 8)
|
668
|
+
plt.xlabel("time")
|
669
|
+
plt.ylabel(r"$x$", rotation='horizontal')
|
670
|
+
plt.axhline(y=5, color='g', linestyle='--', linewidth=1)
|
671
|
+
plt.axhline(y=3.084, color='r', linestyle='--', linewidth=1)
|
373
672
|
|
374
|
-
# gp.plot_crn(rxns, inits, t_eval, figure_size=figsize)
|
375
|
-
plot_tn(odes, inits, t_eval, gamma=gamma, beta=beta)
|
376
673
|
|
377
674
|
|
378
675
|
if __name__ == '__main__':
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ode2tn
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.4
|
4
4
|
Summary: A Python package to turn arbitrary polynomial ODEs into a transcriptional network simulating it.
|
5
5
|
Author-email: Dave Doty <doty@ucdavis.edu>
|
6
6
|
License-Expression: MIT
|
@@ -14,7 +14,7 @@ Requires-Dist: scipy>=1.15
|
|
14
14
|
Requires-Dist: sympy>=1.13
|
15
15
|
Dynamic: license-file
|
16
16
|
|
17
|
-
#
|
17
|
+
# ode2tn
|
18
18
|
ode2tn is a Python package to compile arbitrary polynomial ODEs into a transcriptional network simulating the ODEs.
|
19
19
|
|
20
20
|
See this paper for details: TODO
|
@@ -25,11 +25,11 @@ Type `pip install ode2tn` at the command line.
|
|
25
25
|
|
26
26
|
## Usage
|
27
27
|
|
28
|
-
See the [notebook.ipynb](notebook.ipynb) for more examples of usage.
|
28
|
+
See the [notebook.ipynb](notebook.ipynb) (relative link on GitHub; will not work on other sites like pypi.org) for more examples of usage, including all the examples discussed in the paper.
|
29
29
|
|
30
30
|
The functions `ode2tn` and `plot_tn` are the main elements of the package.
|
31
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
|
32
|
+
Each variable $x$ in the original ODEs is represented by a pair of variables $x^\top,x^\bot$, whose ratio $\frac{x^\top}{x^\bot}$ follows the same dynamics in the transcriptional network as $x$ does in the original ODEs.
|
33
33
|
`plot_tn` does this conversion and then plots the ratios by default, although it can be customized what exactly is plotted;
|
34
34
|
see the documentation for [gpac.plot](https://gpac.readthedocs.io/en/latest/#gpac.ode.plot) for a description of all options.
|
35
35
|
|
@@ -42,41 +42,63 @@ import sympy as sp
|
|
42
42
|
from transform import plot_tn, ode2tn
|
43
43
|
|
44
44
|
x,y = sp.symbols('x y')
|
45
|
-
odes = {
|
46
|
-
x: y-2,
|
47
|
-
y: -x+2,
|
45
|
+
odes = { # odes dict maps each symbol to an expression for its time derivative
|
46
|
+
x: y-2, # dx/dt = y-2
|
47
|
+
y: -x+2, # dy/dt = -x+2
|
48
48
|
}
|
49
|
-
inits = {
|
50
|
-
x: 2,
|
51
|
-
y: 1,
|
49
|
+
inits = { # inits maps each symbol to its initial value
|
50
|
+
x: 2, # x(0) = 2
|
51
|
+
y: 1, # y(0) = 1
|
52
52
|
}
|
53
|
-
gamma = 2
|
54
|
-
|
55
|
-
|
56
|
-
|
53
|
+
gamma = 2 # uniform decay constant; should have gamma > max q^-;
|
54
|
+
# see proof of main Theorem in paper for what q^- is
|
55
|
+
beta = 1 # constant introduced to keep values from going to infinity or 0
|
56
|
+
tn_odes, tn_inits, tn_syms = ode2tn(odes, inits, gamma=gamma, beta=beta)
|
57
|
+
gp.display_odes(tn_odes) # displays nice rendered LaTeX in Jupyter notebook
|
58
|
+
print(f'{tn_inits=}')
|
59
|
+
print(f'{tn_syms=}')
|
57
60
|
```
|
58
61
|
|
59
|
-
|
62
|
+
When run in a Jupyter notebook, this will show
|
63
|
+
|
64
|
+

|
65
|
+
|
66
|
+
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.
|
60
67
|
|
68
|
+
If not in a Jupyter notebook, one could also inspect the transcriptional network ODEs via
|
69
|
+
```python
|
70
|
+
for var, ode in tn_odes.items():
|
71
|
+
print(f"{var}' = {ode}")
|
72
|
+
```
|
73
|
+
which would print a text-based version of the equations:
|
61
74
|
```
|
62
75
|
x_t' = x_b*y_t/y_b - 2*x_t + x_t/x_b
|
63
76
|
x_b' = 2*x_b**2/x_t - 2*x_b + 1
|
64
77
|
y_t' = 2*y_b - 2*y_t + y_t/y_b
|
65
78
|
y_b' = -2*y_b + 1 + x_t*y_b**2/(x_b*y_t)
|
66
|
-
tn_inits={x_t: 2, x_b: 1, y_t: 1, y_b: 1}
|
67
|
-
tn_syms={x: (x_t, x_b), y: (y_t, y_b)}
|
68
79
|
```
|
69
80
|
|
70
|
-
|
71
|
-
|
72
|
-
Running the code above in a Jupyter notebook will print the above text and show this figure:
|
81
|
+
The function `plot_tn` above does this conversion on the *original* odes and then plots the ratios.
|
82
|
+
Running
|
73
83
|
|
74
|
-
|
84
|
+
```python
|
85
|
+
t_eval = np.linspace(0, 6*pi, 1000)
|
86
|
+
# note below it is odes and inits, not tn_odes and tn_inits
|
87
|
+
# plot_tn calls ode2tn to convert the ODEs before plotting
|
88
|
+
plot_tn(odes, inits, gamma=gamma, beta=beta, t_eval=t_eval, show_factors=True)
|
89
|
+
```
|
90
|
+
|
91
|
+
in a Jupyter notebook will show this figure:
|
75
92
|
|
76
|
-
|
93
|
+

|
94
|
+
|
95
|
+
The parameter `show_factors` above indicates to show a second subplot with the underlying transcription factors ($x^\top, x^\bot, y^\top, y^\bot$ above).
|
96
|
+
If left unspecified, it defaults to `False` and plots only the original values (ratios of pairs of transcription factors, $x,y$ above).
|
97
|
+
|
98
|
+
One could also hand the transcriptional network ODEs to [gpac](https://github.com/UC-Davis-molecular-computing/gpac) to integrate, if you want to directly access the data being plotted above.
|
77
99
|
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`:
|
78
100
|
|
79
|
-
```
|
101
|
+
```python
|
80
102
|
t_eval = np.linspace(0, 2*pi, 5)
|
81
103
|
sol = gp.integrate_odes(tn_odes, tn_inits, t_eval)
|
82
104
|
print(f'times = {sol.t}')
|
@@ -0,0 +1,7 @@
|
|
1
|
+
ode2tn/__init__.py,sha256=CEHlHkTMSItKXBPVGGxTNa9FO-ywsIQ5-v6xk7Oy91A,177
|
2
|
+
ode2tn/transform.py,sha256=rM-VB22RzXkJBFB3FQXRNbW9o8Im2uB2BDZmd-IqKQo,28197
|
3
|
+
ode2tn-1.0.4.dist-info/licenses/LICENSE,sha256=VV9UH0kkG-2edZvwJOqgtN12bZIzs2vn9_cq1SjoUJc,1091
|
4
|
+
ode2tn-1.0.4.dist-info/METADATA,sha256=DdtTkq9m5Da2Ui2xI_eZZIwIFgYzzGF2pXAMm8QFHqQ,5212
|
5
|
+
ode2tn-1.0.4.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
6
|
+
ode2tn-1.0.4.dist-info/top_level.txt,sha256=fPQ9s5yLIYfazJS7wBBfU9EsWa9RGALq8VL-wUYRlao,7
|
7
|
+
ode2tn-1.0.4.dist-info/RECORD,,
|
ode2tn-1.0.2.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
ode2tn/__init__.py,sha256=b_mINIsNfCWzgG7QVYMsRsWKDLvp2QKFAzRqWtYqwDA,30
|
2
|
-
ode2tn/transform.py,sha256=8gQVHaCRTdiACQPZdkB3OPfc2KOZTA3YwMgbXDDnNkE,16155
|
3
|
-
ode2tn-1.0.2.dist-info/licenses/LICENSE,sha256=VV9UH0kkG-2edZvwJOqgtN12bZIzs2vn9_cq1SjoUJc,1091
|
4
|
-
ode2tn-1.0.2.dist-info/METADATA,sha256=MSGeKttEB-ImlNvBof07AeyxR1rOCBf48tUNinaIgS0,4095
|
5
|
-
ode2tn-1.0.2.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
6
|
-
ode2tn-1.0.2.dist-info/top_level.txt,sha256=fPQ9s5yLIYfazJS7wBBfU9EsWa9RGALq8VL-wUYRlao,7
|
7
|
-
ode2tn-1.0.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|