ode2tn 1.0.3__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 +347 -58
- {ode2tn-1.0.3.dist-info → ode2tn-1.0.4.dist-info}/METADATA +15 -14
- ode2tn-1.0.4.dist-info/RECORD +7 -0
- ode2tn-1.0.3.dist-info/RECORD +0 -7
- {ode2tn-1.0.3.dist-info → ode2tn-1.0.4.dist-info}/WHEEL +0 -0
- {ode2tn-1.0.3.dist-info → ode2tn-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {ode2tn-1.0.3.dist-info → ode2tn-1.0.4.dist-info}/top_level.txt +0 -0
ode2tn/__init__.py
CHANGED
ode2tn/transform.py
CHANGED
@@ -16,6 +16,8 @@ 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,
|
21
23
|
latex_legend: bool = True,
|
@@ -49,25 +51,32 @@ def plot_tn(
|
|
49
51
|
dict of sp symbols or strings (representing symbols) to sympy expressions or strings or floats
|
50
52
|
(representing RHS of ODEs)
|
51
53
|
Raises ValueError if any of the ODEs RHS is not a polynomial
|
54
|
+
|
52
55
|
initial_values: initial values,
|
53
56
|
dict of sympy symbols or strings (representing symbols) to floats
|
57
|
+
|
54
58
|
gamma: coefficient of the negative linear term in the transcription network
|
59
|
+
|
55
60
|
beta: additive constant in x_top ODE
|
61
|
+
|
56
62
|
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
57
63
|
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
58
64
|
`x*scale`.
|
65
|
+
|
59
66
|
show_factors: if True, then in addition to plotting the ratios x_top/x_bot,
|
60
67
|
also plot the factors x_top and x_bot separately in a second plot.
|
61
68
|
Mutually exlusive with `symbols_to_plot`, since it is equivalent to setting
|
62
69
|
`symbols_to_plot` to ``[ratios, factors]``, where ratios is a list of dependent symbols
|
63
70
|
`x=x_top/x_bot`, and factors is a list of symbols with the transcription factors `x_top`, `x_bot`,
|
64
71
|
for each original variable `x`.
|
72
|
+
|
65
73
|
latex_legend: If True, surround each symbol name with dollar signs, unless it is already surrounded with them,
|
66
74
|
so that the legend is interpreted as LaTeX. If this is True, then the symbol name must either
|
67
75
|
start and end with `$`, or neither start nor end with `$`. Unlike in the gpac package, this is True
|
68
76
|
by default. The names of transcription factors are automatically surrounded by dollar signs.
|
69
77
|
This option makes sure the legend showing original variables (or dependent symbols) also have `$` added
|
70
78
|
so as to be interpreted as LaTeX.
|
79
|
+
|
71
80
|
resets:
|
72
81
|
If specified, this is a dict mapping times to "configurations"
|
73
82
|
(i.e., dict mapping symbols/str to values).
|
@@ -87,6 +96,26 @@ def plot_tn(
|
|
87
96
|
if a symbol is invalid, or if there are symbols representing both an original variable `x` and one of
|
88
97
|
its `x_top` or `x_bot` variables.
|
89
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
|
+
|
90
119
|
Returns:
|
91
120
|
Typically None, but if return_ode_result is True, returns the result of the ODE integration.
|
92
121
|
See documentation of `gpac.plot` for details.
|
@@ -94,7 +123,9 @@ def plot_tn(
|
|
94
123
|
if show_factors and symbols_to_plot is not None:
|
95
124
|
raise ValueError("Cannot use both show_factors and symbols_to_plot at the same time.")
|
96
125
|
|
97
|
-
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)
|
98
129
|
dependent_symbols_tn = dict(dependent_symbols) if dependent_symbols is not None else {}
|
99
130
|
tn_ratios = {sym: sym_t/sym_b for sym, (sym_t, sym_b) in tn_syms.items()}
|
100
131
|
dependent_symbols_tn.update(tn_ratios)
|
@@ -178,6 +209,9 @@ def ode2tn(
|
|
178
209
|
gamma: float,
|
179
210
|
beta: float,
|
180
211
|
scale: float = 1.0,
|
212
|
+
ignore: Iterable[sp.Symbol] = (),
|
213
|
+
existing_factors: Iterable[sp.Symbol | str] = (),
|
214
|
+
verify_negative_term: bool = True,
|
181
215
|
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]]]:
|
182
216
|
"""
|
183
217
|
Maps polynomial ODEs and and initial values to transcription network (represented by ODEs with positive
|
@@ -188,15 +222,43 @@ def ode2tn(
|
|
188
222
|
dict of sympy symbols or strings (representing symbols) to sympy expressions or strings or floats
|
189
223
|
(representing RHS of ODEs)
|
190
224
|
Raises ValueError if any of the ODEs RHS is not a polynomial
|
225
|
+
|
191
226
|
initial_values: initial values,
|
192
227
|
dict of sympy symbols or strings (representing symbols) or gpac.Specie (representing chemical
|
193
228
|
species, if the ODEs were derived from `gpac.crn_to_odes`) to floats
|
229
|
+
|
194
230
|
gamma: coefficient of the negative linear term in the transcription network
|
231
|
+
|
195
232
|
beta: additive constant in x_top ODE
|
233
|
+
|
196
234
|
scale: "scaling factor" for the transcription network ODEs. Each variable `x` is replaced by a pair
|
197
235
|
(`x_top`, `x_bot`). The initial `x_bot` value is `scale`, and the initial `x_top` value is
|
198
236
|
`x*scale`.
|
199
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
|
+
|
200
262
|
Return:
|
201
263
|
triple (`tn_odes`, `tn_inits`, `tn_syms`), where `tn_syms` is a dict mapping each original symbol ``x``
|
202
264
|
in the original ODEs to the pair ``(x_top, x_bot)``.
|
@@ -211,6 +273,10 @@ def ode2tn(
|
|
211
273
|
initial_values_norm[symbol] = value
|
212
274
|
initial_values = initial_values_norm
|
213
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
|
+
|
214
280
|
# normalize odes dict to use symbols as keys
|
215
281
|
odes_normalized = {}
|
216
282
|
symbols_found_in_expressions = set()
|
@@ -245,8 +311,156 @@ def ode2tn(
|
|
245
311
|
if not expr.is_polynomial():
|
246
312
|
raise ValueError(f"ODE for {symbol}' is not a polynomial: {expr}")
|
247
313
|
|
248
|
-
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.
|
249
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))
|
250
464
|
|
251
465
|
def normalized_ode2tn(
|
252
466
|
odes: dict[sp.Symbol, sp.Expr],
|
@@ -255,34 +469,51 @@ def normalized_ode2tn(
|
|
255
469
|
gamma: float,
|
256
470
|
beta: float,
|
257
471
|
scale: float,
|
472
|
+
ignore: list[sp.Symbol],
|
473
|
+
existing_factors: list[sp.Symbol],
|
474
|
+
verify_negative_term: bool,
|
258
475
|
) -> tuple[dict[sp.Symbol, sp.Expr], dict[sp.Symbol, float], dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]]]:
|
259
476
|
# Assumes ode2tn has normalized and done error-checking
|
260
477
|
|
261
478
|
tn_syms: dict[sp.Symbol, tuple[sp.Symbol, sp.Symbol]] = {}
|
262
479
|
for x in odes.keys():
|
263
|
-
|
264
|
-
|
265
|
-
|
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)
|
266
484
|
|
267
485
|
tn_odes: dict[sp.Symbol, sp.Expr] = {}
|
268
486
|
tn_inits: dict[sp.Symbol, float] = {}
|
269
|
-
for x,
|
270
|
-
|
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)
|
271
500
|
|
272
501
|
# replace sym with sym_top / sym_bot for each original symbol sym
|
273
502
|
for sym in odes.keys():
|
503
|
+
if sym in existing_factors or sym in ignore:
|
504
|
+
continue
|
274
505
|
sym_top, sym_bot = tn_syms[sym]
|
275
506
|
ratio = sym_top / sym_bot
|
276
507
|
p_pos = p_pos.subs(sym, ratio)
|
277
508
|
p_neg = p_neg.subs(sym, ratio)
|
278
509
|
|
279
510
|
x_t, x_b = tn_syms[x]
|
280
|
-
# tn_odes[x_top] = beta + p_pos * x_bot - gamma * x_top
|
281
|
-
# tn_odes[x_bot] = p_neg * x_bot ** 2 / x_top + beta * x_bot / x_top - gamma * x_bot
|
282
511
|
tn_odes[x_t] = beta * x_t / x_b + p_pos * x_b - gamma * x_t
|
283
512
|
tn_odes[x_b] = beta + p_neg * x_b ** 2 / x_t - gamma * x_b
|
284
513
|
tn_inits[x_t] = initial_values.get(x, 0) * scale
|
285
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)
|
286
517
|
|
287
518
|
return tn_odes, tn_inits, tn_syms
|
288
519
|
|
@@ -293,22 +524,27 @@ def split_polynomial(expr: sp.Expr) -> tuple[sp.Expr, sp.Expr]:
|
|
293
524
|
p1: monomials with positive coefficients
|
294
525
|
p2: monomials with negative coefficients (made positive)
|
295
526
|
|
296
|
-
|
297
|
-
|
527
|
+
Parameters
|
528
|
+
----------
|
529
|
+
expr: A sympy Expression that is a polynomial
|
298
530
|
|
299
|
-
Returns
|
531
|
+
Returns
|
532
|
+
-------
|
533
|
+
:
|
300
534
|
pair of sympy Expressions (`p1`, `p2`) such that expr = p1 - p2
|
301
535
|
|
302
|
-
Raises
|
303
|
-
|
536
|
+
Raises
|
537
|
+
------
|
538
|
+
ValueError:
|
539
|
+
If `expr` is not a polynomial. Note that the constants (sympy type ``Number``)
|
304
540
|
are not considered polynomials by the ``is_polynomial`` method, but we do consider them polynomials
|
305
541
|
and do not raise an exception in this case.
|
306
542
|
"""
|
307
|
-
if expr.is_constant():
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
543
|
+
# if expr.is_constant():
|
544
|
+
# if expr < 0:
|
545
|
+
# return sp.S(0), -expr
|
546
|
+
# else:
|
547
|
+
# return expr, sp.S(0)
|
312
548
|
|
313
549
|
# Verify it's a polynomial
|
314
550
|
if not expr.is_polynomial():
|
@@ -324,38 +560,15 @@ def split_polynomial(expr: sp.Expr) -> tuple[sp.Expr, sp.Expr]:
|
|
324
560
|
# For a sum, we can process each term
|
325
561
|
if expanded.is_Add:
|
326
562
|
for term in expanded.args:
|
327
|
-
|
328
|
-
|
329
|
-
# For products, find the numeric coefficient
|
330
|
-
coeff = next((arg for arg in term.args if arg.is_number), 1)
|
563
|
+
if is_negative(term):
|
564
|
+
p_neg += -term
|
331
565
|
else:
|
332
|
-
# For non-products (like just x or just a number)
|
333
|
-
coeff = 1 if not term.is_number else term
|
334
|
-
|
335
|
-
# Add to the appropriate part based on sign
|
336
|
-
if coeff > 0:
|
337
566
|
p_pos += term
|
338
|
-
else:
|
339
|
-
# For negative coefficients, add the negated term to p2
|
340
|
-
p_neg += -term
|
341
|
-
elif expanded.is_Mul or expanded.is_Pow:
|
342
|
-
# If it's a single term, just check the sign; is_Mul for things like x*y or -x (represented as -1*x)
|
343
|
-
coeff = next((arg for arg in expanded.args if arg.is_number), 1)
|
344
|
-
if coeff > 0:
|
345
|
-
p_pos = expanded
|
346
|
-
else:
|
347
|
-
p_neg = -expanded
|
348
|
-
elif expanded.is_Atom:
|
349
|
-
# since negative terms are technically Mul, i.e., -1*x, if it is an atom then it is positive
|
350
|
-
p_pos = expanded
|
351
567
|
else:
|
352
|
-
|
353
|
-
# in tests a term like -x is actually represented as -1*x, so that's covered by the above elif,
|
354
|
-
# but in case of a negative constant like -2, this is handled here
|
355
|
-
if expanded > 0:
|
356
|
-
p_pos = expanded
|
357
|
-
else:
|
568
|
+
if is_negative(expanded):
|
358
569
|
p_neg = -expanded
|
570
|
+
else:
|
571
|
+
p_pos = expanded
|
359
572
|
|
360
573
|
return p_pos, p_neg
|
361
574
|
|
@@ -364,23 +577,99 @@ def comma_separated(elts: Iterable[Any]) -> str:
|
|
364
577
|
return ', '.join(str(elt) for elt in elts)
|
365
578
|
|
366
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
|
+
|
367
595
|
def main():
|
596
|
+
from sympy.abc import p,q,w,z,x
|
597
|
+
|
368
598
|
import gpac as gp
|
369
599
|
import numpy as np
|
370
600
|
import sympy as sp
|
371
|
-
from ode2tn import
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
odes = gp.crn_to_odes(rxns)
|
376
|
-
inits = {x: 1}
|
377
|
-
t_eval = np.linspace(0, 1, 100)
|
378
|
-
gamma = 1
|
601
|
+
from ode2tn import ode2tn
|
602
|
+
import matplotlib.pyplot as plt
|
603
|
+
|
604
|
+
gamma = 20
|
379
605
|
beta = 1
|
380
|
-
#
|
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)
|
381
672
|
|
382
|
-
# gp.plot_crn(rxns, inits, t_eval, figure_size=figsize)
|
383
|
-
plot_tn(odes, inits, t_eval, gamma=gamma, beta=beta)
|
384
673
|
|
385
674
|
|
386
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
|
@@ -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,25 +42,26 @@ 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
|
-
|
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
|
55
56
|
tn_odes, tn_inits, tn_syms = ode2tn(odes, inits, gamma=gamma, beta=beta)
|
56
|
-
gp.display_odes(tn_odes)
|
57
|
+
gp.display_odes(tn_odes) # displays nice rendered LaTeX in Jupyter notebook
|
57
58
|
print(f'{tn_inits=}')
|
58
59
|
print(f'{tn_syms=}')
|
59
60
|
```
|
60
61
|
|
61
62
|
When run in a Jupyter notebook, this will show
|
62
63
|
|
63
|
-

|
64
|
+

|
64
65
|
|
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.
|
66
67
|
|
@@ -89,7 +90,7 @@ plot_tn(odes, inits, gamma=gamma, beta=beta, t_eval=t_eval, show_factors=True)
|
|
89
90
|
|
90
91
|
in a Jupyter notebook will show this figure:
|
91
92
|
|
92
|
-

|
93
|
+

|
93
94
|
|
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).
|
95
96
|
If left unspecified, it defaults to `False` and plots only the original values (ratios of pairs of transcription factors, $x,y$ above).
|
@@ -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.3.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
ode2tn/__init__.py,sha256=b_mINIsNfCWzgG7QVYMsRsWKDLvp2QKFAzRqWtYqwDA,30
|
2
|
-
ode2tn/transform.py,sha256=8GciVR2iGRRWgKyvPoYoQ5TZjKrOdNyzaH_ZukJiVsw,16829
|
3
|
-
ode2tn-1.0.3.dist-info/licenses/LICENSE,sha256=VV9UH0kkG-2edZvwJOqgtN12bZIzs2vn9_cq1SjoUJc,1091
|
4
|
-
ode2tn-1.0.3.dist-info/METADATA,sha256=RbwHIvSA6PgVWV9I57Fs9n2OdU7ocksS65YPaxs7174,4882
|
5
|
-
ode2tn-1.0.3.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
6
|
-
ode2tn-1.0.3.dist-info/top_level.txt,sha256=fPQ9s5yLIYfazJS7wBBfU9EsWa9RGALq8VL-wUYRlao,7
|
7
|
-
ode2tn-1.0.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|