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 CHANGED
@@ -1 +1,8 @@
1
- from ode2tn.transform import *
1
+ from ode2tn.transform import (
2
+ ode2tn,
3
+ plot_tn,
4
+ update_resets_with_ratios,
5
+ check_x_is_transcription_factor,
6
+ is_laurent_monomial,
7
+ is_laurent_polynomial,
8
+ )
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
- # create x_t, x_b for each symbol x
256
- x_t, x_b = sp.symbols(f'{x}_t {x}_b')
257
- tn_syms[x] = (x_t, x_b)
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, expr in odes.items():
262
- p_pos, p_neg = split_polynomial(expr)
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
- Args:
289
- expr: A sympy Expression that is a polynomial
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
- ValueError: If `expr` is not a polynomial. Note that the constants (sympy type ``Number``)
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
- if expr < 0:
301
- return sp.S(0), -expr
302
- else:
303
- return expr, sp.S(0)
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
- # Get the coefficient
320
- if term.is_Mul:
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
- # For single constant terms without multiplication, just check the sign;
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 plot_tn
364
-
365
- x = gp.species('X')
366
- rxns = [2 * x >> 3 * x]
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
- # figsize = (16,4)
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.2
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
- # ode-to-transcription-network
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 $x^\top / x^\bot$ follows the same dynamics in the transcriptional network as $x$ does in the original ODEs.
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 = { # odes dict maps each symbol to an expression for its time derivative
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 = { # inits maps each symbol to its initial value
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 # 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
- plot_tn(odes, inits, gamma=gamma, beta=beta, t_eval=t_eval, show_factors=True)
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
- This will print
62
+ When run in a Jupyter notebook, this will show
63
+
64
+ ![](images/ode-display.png)
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
- 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.
71
- The function `plot_tn` above does this conversion and then plots the ratios.
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
- ![](sine-cosine-plot.svg)
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
- One could also hand the transcriptional network ODEs to gpac to integrate, if you want to directly access the data being plotted above.
93
+ ![](images/sine-cosine-plot.svg)
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,,
@@ -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