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 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,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
- # create x_t, x_b for each symbol x
264
- x_t, x_b = sp.symbols(f'{x}_t {x}_b')
265
- 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)
266
484
 
267
485
  tn_odes: dict[sp.Symbol, sp.Expr] = {}
268
486
  tn_inits: dict[sp.Symbol, float] = {}
269
- for x, expr in odes.items():
270
- 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)
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
- Args:
297
- expr: A sympy Expression that is a polynomial
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
- 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``)
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
- if expr < 0:
309
- return sp.S(0), -expr
310
- else:
311
- 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)
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
- # Get the coefficient
328
- if term.is_Mul:
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
- # For single constant terms without multiplication, just check the sign;
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 plot_tn
372
-
373
- x = gp.species('X')
374
- rxns = [2 * x >> 3 * x]
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
- # 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)
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
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 $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,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 = { # 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
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
- ![](ode-display.png)
64
+ ![](images/ode-display.png)
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
- ![](sine-cosine-plot.svg)
93
+ ![](images/sine-cosine-plot.svg)
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,,
@@ -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