xpplab 0.1.1__py3-none-win_amd64.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.
Files changed (221) hide show
  1. xpplab/__init__.py +23 -0
  2. xpplab/_docs/auto.md +102 -0
  3. xpplab/_docs/odefile.md +88 -0
  4. xpplab/_engine.py +331 -0
  5. xpplab/_lib/libxppe.dll +0 -0
  6. xpplab/analysis.py +405 -0
  7. xpplab/animation.py +136 -0
  8. xpplab/auto.py +370 -0
  9. xpplab/examples/6x6.ani +7 -0
  10. xpplab/examples/6x6.ode +47 -0
  11. xpplab/examples/SM2.dat +601 -0
  12. xpplab/examples/SM3.dat +259 -0
  13. xpplab/examples/UM0.dat +601 -0
  14. xpplab/examples/UM1.dat +601 -0
  15. xpplab/examples/V0.tab +14 -0
  16. xpplab/examples/acoaster.ani +17 -0
  17. xpplab/examples/acoaster.ode +49 -0
  18. xpplab/examples/ai.ode +7 -0
  19. xpplab/examples/allinfo.dat +146 -0
  20. xpplab/examples/amari.ode +12 -0
  21. xpplab/examples/amari2.ode +12 -0
  22. xpplab/examples/amarig.ode +12 -0
  23. xpplab/examples/angela.ode +5 -0
  24. xpplab/examples/atcoaster.ani +16 -0
  25. xpplab/examples/atcoaster.ode +49 -0
  26. xpplab/examples/bnn.ode +8 -0
  27. xpplab/examples/bob.ode +3 -0
  28. xpplab/examples/borcol.ode +11 -0
  29. xpplab/examples/cable.ode +20 -0
  30. xpplab/examples/candelator.ode +16 -0
  31. xpplab/examples/chemo.ani +3 -0
  32. xpplab/examples/chemotax.ode +12 -0
  33. xpplab/examples/clustor.ode +26 -0
  34. xpplab/examples/coaster2D.ani +16 -0
  35. xpplab/examples/coaster2D.ode +46 -0
  36. xpplab/examples/cobweb2.ode +26 -0
  37. xpplab/examples/crazy.ode +5 -0
  38. xpplab/examples/cuplamdif.ode +23 -0
  39. xpplab/examples/curve.dat +100 -0
  40. xpplab/examples/curve2.dat +100 -0
  41. xpplab/examples/curve3.dat +100 -0
  42. xpplab/examples/curve4.dat +100 -0
  43. xpplab/examples/dae.ode +10 -0
  44. xpplab/examples/dae_ex1.ode +7 -0
  45. xpplab/examples/dae_ex2.ode +7 -0
  46. xpplab/examples/dae_ex3.ode +10 -0
  47. xpplab/examples/dde.ode +7 -0
  48. xpplab/examples/del_log.ode +9 -0
  49. xpplab/examples/delay.ode +17 -0
  50. xpplab/examples/delta.ode +8 -0
  51. xpplab/examples/diagram.dat +146 -0
  52. xpplab/examples/dirfields.dat +289 -0
  53. xpplab/examples/dlog.ode +6 -0
  54. xpplab/examples/doubpend.ani +7 -0
  55. xpplab/examples/doubpend.ode +11 -0
  56. xpplab/examples/duck.ani +11 -0
  57. xpplab/examples/duck.ode +31 -0
  58. xpplab/examples/duck2.ani +13 -0
  59. xpplab/examples/duck3.ani +18 -0
  60. xpplab/examples/duckx.ode +31 -0
  61. xpplab/examples/dumbbvp.ode +8 -0
  62. xpplab/examples/ecofilip.ode +7 -0
  63. xpplab/examples/efm.ode +9 -0
  64. xpplab/examples/efmplot.ode +9 -0
  65. xpplab/examples/efmx.ode +11 -0
  66. xpplab/examples/efmxfb.ode +12 -0
  67. xpplab/examples/efmxplot.ode +22 -0
  68. xpplab/examples/efmxplott.ode +26 -0
  69. xpplab/examples/efmxrun.ode +11 -0
  70. xpplab/examples/elaspen.ani +7 -0
  71. xpplab/examples/elaspen.ode +22 -0
  72. xpplab/examples/equil.dat +2 -0
  73. xpplab/examples/ev1.ode +16 -0
  74. xpplab/examples/evelyn.ode +16 -0
  75. xpplab/examples/fancylorenz.ode +28 -0
  76. xpplab/examples/fhn.ode +10 -0
  77. xpplab/examples/fhn3d.ode +8 -0
  78. xpplab/examples/fhn_noise.ode +8 -0
  79. xpplab/examples/fieldnoy.ode +17 -0
  80. xpplab/examples/fish.ani +6 -0
  81. xpplab/examples/forcpend.ode +6 -0
  82. xpplab/examples/fr.ode +39 -0
  83. xpplab/examples/gausvolt.ode +5 -0
  84. xpplab/examples/geisel.ode +10 -0
  85. xpplab/examples/getfrefm.ode +15 -0
  86. xpplab/examples/gill_bruss.ode +22 -0
  87. xpplab/examples/greg.ode +13 -0
  88. xpplab/examples/hedge.ode +2 -0
  89. xpplab/examples/henhei.ode +12 -0
  90. xpplab/examples/hhred.ode +29 -0
  91. xpplab/examples/iaf.ode +4 -0
  92. xpplab/examples/idoubpend.ani +13 -0
  93. xpplab/examples/idoubpend.ode +16 -0
  94. xpplab/examples/ielaspen.ani +20 -0
  95. xpplab/examples/ielaspen.ode +25 -0
  96. xpplab/examples/invpend.ani +7 -0
  97. xpplab/examples/invpend.ode +12 -0
  98. xpplab/examples/itoy.ani +27 -0
  99. xpplab/examples/itoy.ode +103 -0
  100. xpplab/examples/jcoaster.ani +20 -0
  101. xpplab/examples/jcoaster.ode +80 -0
  102. xpplab/examples/jcoaster.old.ani +16 -0
  103. xpplab/examples/jcoaster1.ani +20 -0
  104. xpplab/examples/jcoaster1.ode +80 -0
  105. xpplab/examples/julia.ode +10 -0
  106. xpplab/examples/junk.ode +10 -0
  107. xpplab/examples/junk2.ode +9 -0
  108. xpplab/examples/kepler.ode +14 -0
  109. xpplab/examples/koho.ode +46 -0
  110. xpplab/examples/koho2.ode +38 -0
  111. xpplab/examples/koho3.ode +40 -0
  112. xpplab/examples/kohonen.ode +31 -0
  113. xpplab/examples/kohox.ode +11 -0
  114. xpplab/examples/kuramot100.ode +9 -0
  115. xpplab/examples/kuramot100m.ode +11 -0
  116. xpplab/examples/kuramot100w.ode +10 -0
  117. xpplab/examples/lamomeg.ode +16 -0
  118. xpplab/examples/lamprey.ode +13 -0
  119. xpplab/examples/lamvolt.ode +26 -0
  120. xpplab/examples/lecar (Dan Dougherty's conflicted copy 2017-01-01).ode +60 -0
  121. xpplab/examples/lecar (Dan Dougherty's conflicted copy 2017-06-03).ode +60 -0
  122. xpplab/examples/lecar.ode +61 -0
  123. xpplab/examples/lecdiff200.ode +31 -0
  124. xpplab/examples/lecsimp2.ode +31 -0
  125. xpplab/examples/lecwave.ode +27 -0
  126. xpplab/examples/lin.ode +17 -0
  127. xpplab/examples/linc.ode +5 -0
  128. xpplab/examples/lo.ode +7 -0
  129. xpplab/examples/logbif.ode +21 -0
  130. xpplab/examples/lor2.ode +17 -0
  131. xpplab/examples/lorenz.ani +11 -0
  132. xpplab/examples/lorenz.ode +13 -0
  133. xpplab/examples/lorenz2.ode +28 -0
  134. xpplab/examples/lorknead.ode +19 -0
  135. xpplab/examples/lorliap.ode +21 -0
  136. xpplab/examples/lorpecor.ode +23 -0
  137. xpplab/examples/minode.ode +22 -0
  138. xpplab/examples/ml-range.ode +34 -0
  139. xpplab/examples/ml.ani +2 -0
  140. xpplab/examples/ml1.ode +32 -0
  141. xpplab/examples/mlvolt.ode +18 -0
  142. xpplab/examples/myret.ode +7 -0
  143. xpplab/examples/nc.ani +5 -0
  144. xpplab/examples/nf3.ode +17 -0
  145. xpplab/examples/nnet.ode +8 -0
  146. xpplab/examples/nnet2.ode +9 -0
  147. xpplab/examples/nochaos.ode +6 -0
  148. xpplab/examples/nthet.ode +10 -0
  149. xpplab/examples/nullclines.dat +1221 -0
  150. xpplab/examples/osc.ani +32 -0
  151. xpplab/examples/osc.ode +18 -0
  152. xpplab/examples/osc1.ani +32 -0
  153. xpplab/examples/pHtools_ml.ode +51 -0
  154. xpplab/examples/pHtools_sillen.ode +53 -0
  155. xpplab/examples/pHtools_simple.ode +8 -0
  156. xpplab/examples/pend-cart1-kick.ode +35 -0
  157. xpplab/examples/pend-cart1.ani +4 -0
  158. xpplab/examples/pend-cart1.ode +30 -0
  159. xpplab/examples/pend.ani +26 -0
  160. xpplab/examples/pend.ode +21 -0
  161. xpplab/examples/pend2.ani +35 -0
  162. xpplab/examples/pendx.ani +30 -0
  163. xpplab/examples/pendx.ode +11 -0
  164. xpplab/examples/phs.ode +7 -0
  165. xpplab/examples/pp.ode +10 -0
  166. xpplab/examples/r3b.ode +13 -0
  167. xpplab/examples/ross-orbit.ode +7 -0
  168. xpplab/examples/rosscmpliap.ode +88 -0
  169. xpplab/examples/rossler-pecora.ode +23 -0
  170. xpplab/examples/rossler.ode +6 -0
  171. xpplab/examples/rossliap2d.dat +2601 -0
  172. xpplab/examples/simplecube.ode +7 -0
  173. xpplab/examples/sine-circel.ode +12 -0
  174. xpplab/examples/test.ode +4 -0
  175. xpplab/examples/testdll.ode +7 -0
  176. xpplab/examples/torus.ode +9 -0
  177. xpplab/examples/toy.ani +20 -0
  178. xpplab/examples/toy_ok.ode +104 -0
  179. xpplab/examples/transient.ode +8 -0
  180. xpplab/examples/triple.ode +5 -0
  181. xpplab/examples/tst.ode +3 -0
  182. xpplab/examples/tstar.ode +2 -0
  183. xpplab/examples/tstdel.ode +11 -0
  184. xpplab/examples/tstdelsp.ode +12 -0
  185. xpplab/examples/tstdll.ode +61 -0
  186. xpplab/examples/tstdll2.ode +11 -0
  187. xpplab/examples/tstset.ode +4 -0
  188. xpplab/examples/tstvol2.ode +8 -0
  189. xpplab/examples/tstvol3.ode +7 -0
  190. xpplab/examples/vanderpol.ode +7 -0
  191. xpplab/examples/vdp.ode +10 -0
  192. xpplab/examples/vlsi.ode +15 -0
  193. xpplab/examples/voltex1.ode +4 -0
  194. xpplab/examples/voltex2.ode +4 -0
  195. xpplab/examples/voltex3.ode +4 -0
  196. xpplab/examples/voltex4.ode +6 -0
  197. xpplab/examples/vtst.ode +10 -0
  198. xpplab/examples/waterwheel.ani +9 -0
  199. xpplab/examples/waterwheel.ode +21 -0
  200. xpplab/examples/wave (Dan Dougherty's conflicted copy 2017-01-01).ode +15 -0
  201. xpplab/examples/wave (Dan Dougherty's conflicted copy 2017-06-03).ode +15 -0
  202. xpplab/examples/wave.ani +3 -0
  203. xpplab/examples/wave.ode +16 -0
  204. xpplab/examples/wavepp.ani +3 -0
  205. xpplab/examples/wcring.ode +6 -0
  206. xpplab/examples/wcstim.ani +4 -0
  207. xpplab/examples/wcstim.ode +10 -0
  208. xpplab/examples/wta.ode +13 -0
  209. xpplab/mcp/__init__.py +39 -0
  210. xpplab/mcp/__main__.py +7 -0
  211. xpplab/mcp/diagnose.py +252 -0
  212. xpplab/mcp/server.py +515 -0
  213. xpplab/mcp/session.py +1657 -0
  214. xpplab/model.py +556 -0
  215. xpplab/plotting.py +165 -0
  216. xpplab-0.1.1.dist-info/METADATA +84 -0
  217. xpplab-0.1.1.dist-info/RECORD +221 -0
  218. xpplab-0.1.1.dist-info/WHEEL +5 -0
  219. xpplab-0.1.1.dist-info/entry_points.txt +3 -0
  220. xpplab-0.1.1.dist-info/licenses/LICENSE +282 -0
  221. xpplab-0.1.1.dist-info/licenses/engine/xppaut/LICENSE +282 -0
xpplab/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """XPP Lab: XPPAUT's simulation and AUTO bifurcation engine for Python.
2
+
3
+ import xpplab as xs
4
+ m = xs.load("lecar.ode")
5
+ sol = m.run(total=100)
6
+ d = m.auto.equilibria("iapp")
7
+ d.special()
8
+
9
+ The numerics are XPPAUT 8.0's own code, compiled into a native
10
+ library. No Fortran or C compiler and no X11 server are needed.
11
+ """
12
+
13
+ from ._engine import EngineError, find_engine_library
14
+ from .analysis import Table
15
+ from .animation import Animation
16
+ from .auto import Diagram, Point
17
+ from .model import METHODS, Equilibrium, Model, Trajectory, load
18
+
19
+ __all__ = ["Model", "Trajectory", "Equilibrium", "Diagram", "Point", "Table", "Animation",
20
+ "EngineError", "METHODS", "load", "find_engine_library",
21
+ "__version__"]
22
+
23
+ __version__ = "0.1.1"
xpplab/_docs/auto.md ADDED
@@ -0,0 +1,102 @@
1
+ # Bifurcation analysis with AUTO
2
+
3
+ A **bifurcation diagram** shows how the long-term behaviour of a system
4
+ changes as one parameter varies: where equilibria appear, vanish or lose
5
+ stability, and where oscillations are born. XPP Lab computes these
6
+ diagrams with **AUTO** (E. Doedel), the same continuation code that XPPAUT
7
+ embeds.
8
+
9
+ ## The idea in brief
10
+
11
+ Pick a parameter, say a current `I`. AUTO starts from an equilibrium and
12
+ follows it step by step as `I` changes. At each step it solves for the
13
+ equilibrium and checks its stability. When something qualitative happens it
14
+ marks a **special point**:
15
+
16
+ | Label | Name | What happens |
17
+ |---|---|---|
18
+ | LP | limit point (fold, saddle-node) | Two equilibria meet and vanish; the branch turns back. |
19
+ | HB | Hopf bifurcation | A pair of eigenvalues crosses the imaginary axis; periodic orbits are born. |
20
+ | BP | branch point | Two branches cross (common with symmetry). |
21
+ | PD | period doubling | An orbit loses stability to one of twice the period. |
22
+ | TR | torus (Neimark-Sacker) | An orbit loses stability to quasi-periodic motion. |
23
+ | LP (on orbits) | fold of limit cycles | Two periodic orbits meet and vanish. |
24
+ | EP | end point | Where the computation stopped. |
25
+
26
+ Stable parts are drawn solid, unstable parts dashed. The colours follow
27
+ XPPAUT: steady states are red (stable) and black (unstable); periodic orbits
28
+ are green (stable) and blue (unstable).
29
+
30
+ ## A typical session
31
+
32
+ 1. **Start at an equilibrium.** Run the model until it settles, or press
33
+ **Equilibrium**. **Follow steady states** also polishes the start point
34
+ with Newton's method, but AUTO needs to begin close to an equilibrium.
35
+ 2. **Choose the parameter** and its range (**Par min** and **Par max**).
36
+ 3. **Follow steady states.** The special points appear in the table and on
37
+ the plot.
38
+ 4. **Select a special point** (click it in the table or the plot) and choose
39
+ what to do from there:
40
+ - from **HB**: *Periodic orbits from this Hopf point*, or a two-parameter
41
+ curve of Hopf points;
42
+ - from **LP**: a two-parameter curve of folds;
43
+ - from **BP**: *Switch to the other branch*;
44
+ - from **PD**: the period-doubled branch.
45
+ 5. For two-parameter curves, choose the **2nd param.** first. The result
46
+ appears in the **Two parameters** tab.
47
+
48
+ Try it: [open the Morris-Lecar model](example:lecar.ode), apply the `hopf`
49
+ set (Workspace > Sets), then [follow the steady states in iapp](cmd:auto steady iapp).
50
+
51
+ ## Check the diagram with simulations
52
+
53
+ **Workspace > Range** (XPPAUT's *Initial conds > Range*) runs the model
54
+ once for each value of a parameter, and in *Max / min* mode records the
55
+ maximum and minimum of a variable after a transient. When the parameter and
56
+ the plotted variable match the Bifurcation window's, the results appear as
57
+ grey dots on AUTO's diagram. They should sit on the stable branches: the
58
+ solid red lines and the solid green ones. Where the system is bistable,
59
+ different initial values land on different branches; tick *Start each run
60
+ where the last ended* to follow one branch as the parameter moves.
61
+
62
+ ## The numerical settings
63
+
64
+ | Setting | Meaning | Advice |
65
+ |---|---|---|
66
+ | Step DS | Size of the first step. **Its sign is the direction.** | Use a negative DS to explore towards smaller parameter values. |
67
+ | DS min, DS max | Bounds on the adaptive step. | A smaller DS max gives smoother curves and fewer missed points. |
68
+ | Par min, Par max | The parameter range. | AUTO stops at the edges. |
69
+ | Max steps (NMAX) | Most steps per run. | A closed loop of orbits runs until NMAX: lower it to stop sooner. |
70
+ | Label every (NPR) | Also label a point every N steps. | Labelled points can be continued from. |
71
+ | Mesh NTST | Mesh intervals for periodic orbits. | Increase it (to 50 or 100) for spiky orbits such as action potentials. |
72
+
73
+ ## When things go wrong
74
+
75
+ - **"AUTO computed no points"**: the start point is probably not an
76
+ equilibrium. Run the model to steady state first, or press Equilibrium.
77
+ - **The branch stops early**: check Par min and Par max, and Norm max.
78
+ Then try a smaller DS max.
79
+ - **Periodic orbits fail from a Hopf point**: increase NTST, or reduce DS.
80
+ - **Points come out jagged**: reduce DS max.
81
+
82
+ ## Scripting the same analysis
83
+
84
+ Every step is also available from the command window:
85
+
86
+ ```
87
+ use hopf
88
+ auto set ds=0.02 dsmax=0.05 parmax=0.6
89
+ auto steady iapp
90
+ auto from 2 periodic
91
+ ```
92
+
93
+ and from Python:
94
+
95
+ ```
96
+ import xpplab as xs
97
+ m = xs.load("lecar.ode")
98
+ m.apply_set("hopf")
99
+ d = m.auto.equilibria("iapp")
100
+ orbits = m.auto.continue_from(d.first("HB"), "periodic")
101
+ m.auto.diagram.plot()
102
+ ```
@@ -0,0 +1,88 @@
1
+ # The .ode file format
2
+
3
+ A model is a plain text file, usually ending in `.ode`. XPP Lab reads
4
+ exactly the format of XPPAUT 8.0, so files written for XPPAUT work
5
+ unchanged. Each statement goes on its own line, and the file ends with
6
+ `done`. Upper and lower case are the same in names.
7
+
8
+ ## A complete example
9
+
10
+ ```
11
+ # Lotka-Volterra predator-prey model
12
+ x' = a*x - b*x*y
13
+ y' = -c*y + d*x*y
14
+ par a=1, b=0.5, c=0.75, d=0.25
15
+ init x=2, y=1
16
+ aux total = x + y
17
+ @ total=50, dt=0.02, xp=x, yp=y, xlo=0, xhi=8, ylo=0, yhi=6
18
+ done
19
+ ```
20
+
21
+ ## Equations
22
+
23
+ | Form | Meaning |
24
+ |---|---|
25
+ | `x' = f` or `dx/dt = f` | A differential equation. |
26
+ | `x(t+1) = f` | A map (difference equation); use method `discrete`. |
27
+ | `x(t) = f` | A Volterra integral equation (`f` may contain `int{...}`). |
28
+ | `0 = f` | An algebraic constraint (with `solv` variables: a DAE). |
29
+ | `s = expression` | A fixed (intermediate) quantity, computed at each step. |
30
+ | `aux name = expression` | An auxiliary output: plotted and saved, but not integrated. |
31
+
32
+ ## Declarations
33
+
34
+ | Statement | Meaning |
35
+ |---|---|
36
+ | `par a=1, b=2` | Parameters. |
37
+ | `init x=0.1, y=0` | Initial values (`x(0)=0.1` also works). |
38
+ | `number pi2=6.2832` | A constant that cannot be changed while running. |
39
+ | `f(u,v) = u^2 - v` | A user function, with up to 9 arguments. |
40
+ | `!d = 2*a` | A derived parameter, recomputed when parameters change. |
41
+ | `table g g.tab` | A function tabulated in a file. |
42
+ | `wiener w` | A Wiener process for noise; use `euler` or `backeul`. |
43
+ | `markov z 2` | A Markov chain variable with 2 states (transition table follows). |
44
+ | `global 1 x-1 {x=0}` | An event: when `x-1` crosses zero upwards, reset `x=0`. |
45
+ | `bdry x-1` | A boundary condition for boundary-value problems. |
46
+ | `set name {a=2, total=50}` | A named parameter set. |
47
+ | `#include file.inc` | Insert another file. |
48
+
49
+ Arrays save typing: `x[1..10]' = -x[j] + x[j-1]` defines ten equations,
50
+ with `j` running from 1 to 10.
51
+
52
+ ## Expressions
53
+
54
+ Arithmetic: `+ - * / ^` and parentheses. Comparisons and logic
55
+ (`< > <= >= == != & |`) give 1 or 0, so `(x>0)*x` is max(x, 0).
56
+
57
+ Functions: `sin cos tan asin acos atan atan2 sinh cosh tanh exp ln log
58
+ log10 sqrt abs heav sign mod max min flr ran normal besselj bessely erf erfc
59
+ hom_bcs`, and `if(cond)then(a)else(b)`.
60
+
61
+ Special terms: `t` is time. `delay(x, tau)` or `x(t-tau)` is a delayed
62
+ value. `pi` is 3.14159...
63
+
64
+ ## Options (@ lines)
65
+
66
+ Options set how the model runs. They can be combined on one line:
67
+ `@ total=100, dt=0.05, meth=stiff`.
68
+
69
+ | Option | Meaning |
70
+ |---|---|
71
+ | `total`, `dt`, `t0`, `trans` | Integration time, step, start time, transient to discard. |
72
+ | `meth` | Method: `rungekutta`, `euler`, `modeuler`, `adams`, `gear`, `stiff`, `cvode`, `5dp`, `83dp`, `2rb`, `backeul`, `qualrk`, `volterra`, `ymp`, `discrete`. |
73
+ | `tol`, `atol` | Relative and absolute tolerances of adaptive methods. |
74
+ | `dtmin`, `dtmax` | Limits on the adaptive step. |
75
+ | `bound` | Stop if a variable exceeds this size. |
76
+ | `nout` | Store every n-th step. |
77
+ | `maxstor` | Maximum number of stored points. |
78
+ | `delay` | Maximum delay, for delay equations. |
79
+ | `xp`, `yp`, `xlo`, `xhi`, `ylo`, `yhi` | The phase-plane variables and window. |
80
+ | `nmesh` | Grid for nullclines. |
81
+ | `poimap`, `poivar`, `poipln`, `poisgn` | Poincare sections. |
82
+ | `ds`, `dsmin`, `dsmax`, `parmin`, `parmax`, `nmax`, `npr`, `ntst` | AUTO settings. |
83
+
84
+ ## Comments
85
+
86
+ Lines starting with `#` are comments. The first comment lines of a file are
87
+ shown as its description in the Models window, so describe your model there.
88
+ Lines starting with `"` are XPPAUT's interactive tutorial text.
xpplab/_engine.py ADDED
@@ -0,0 +1,331 @@
1
+ """Low-level access to the XPPAUT engine library (engine/api/xppe.h).
2
+
3
+ XPPAUT keeps its state in global variables, so one loaded copy of the
4
+ engine library holds one model. Each Engine therefore loads its own private
5
+ copy of the library file. The copies are independent: several models can
6
+ be open at once, and closing one frees its memory.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import os
13
+ import shutil
14
+ import sys
15
+ import tempfile
16
+ import threading
17
+ from contextlib import contextmanager
18
+ from pathlib import Path
19
+
20
+ __all__ = ["Engine", "EngineError", "find_engine_library"]
21
+
22
+ API_VERSION = 2
23
+
24
+ OK = 0
25
+ ERR_STATE = -1
26
+ ERR_LOAD = -2
27
+ ERR_FATAL = -3
28
+ ERR_NAME = -4
29
+ ERR_RANGE = -5
30
+ ERR_NUMERIC = -6
31
+
32
+ VARIABLES = 0
33
+ PARAMETERS = 1
34
+ COLUMNS = 2
35
+ SETS = 3
36
+
37
+ AUTO_U0 = 0
38
+ AUTO_UMAX = 1
39
+ AUTO_UMIN = 2
40
+ AUTO_UMEAN = 3
41
+ AUTO_EIG_RE = 4
42
+ AUTO_EIG_IM = 5
43
+ AUTO_PARAMS = 6
44
+
45
+ _ERROR_NAMES = {
46
+ ERR_STATE: "state",
47
+ ERR_LOAD: "load",
48
+ ERR_FATAL: "fatal",
49
+ ERR_NAME: "name",
50
+ ERR_RANGE: "range",
51
+ ERR_NUMERIC: "numeric",
52
+ }
53
+
54
+
55
+ class EngineError(RuntimeError):
56
+ """An error reported by the XPPAUT engine.
57
+
58
+ ``log`` holds everything the engine printed during the failing call,
59
+ which usually explains the problem (for example a syntax error in the
60
+ model file).
61
+ """
62
+
63
+ def __init__(self, message: str, code: int = 0, log: str = ""):
64
+ super().__init__(message)
65
+ self.code = code
66
+ self.kind = _ERROR_NAMES.get(code, "error")
67
+ self.log = log
68
+
69
+ def __str__(self) -> str:
70
+ text = super().__str__()
71
+ if self.log.strip():
72
+ tail = "\n".join(self.log.strip().splitlines()[-12:])
73
+ text += "\n\nEngine output (last lines):\n" + tail
74
+ return text
75
+
76
+
77
+ class AutoPointInfo(ctypes.Structure):
78
+ _fields_ = [
79
+ ("branch", ctypes.c_int),
80
+ ("point", ctypes.c_int),
81
+ ("type", ctypes.c_int),
82
+ ("label", ctypes.c_int),
83
+ ("stable", ctypes.c_int),
84
+ ("periodic", ctypes.c_int),
85
+ ("two_parameter", ctypes.c_int),
86
+ ("par1_slot", ctypes.c_int),
87
+ ("par2_slot", ctypes.c_int),
88
+ ("par1", ctypes.c_double),
89
+ ("par2", ctypes.c_double),
90
+ ("norm", ctypes.c_double),
91
+ ("period", ctypes.c_double),
92
+ ("type_name", ctypes.c_char * 4),
93
+ ]
94
+
95
+
96
+ class AniItem(ctypes.Structure):
97
+ """One shape of an animation frame (xppe_ani_item in xppe.h)."""
98
+ _fields_ = [
99
+ ("shape", ctypes.c_int),
100
+ ("command", ctypes.c_int),
101
+ ("filled", ctypes.c_int),
102
+ ("x1", ctypes.c_double),
103
+ ("y1", ctypes.c_double),
104
+ ("x2", ctypes.c_double),
105
+ ("y2", ctypes.c_double),
106
+ ("color", ctypes.c_int),
107
+ ("level", ctypes.c_double),
108
+ ("thickness", ctypes.c_int),
109
+ ("trail", ctypes.c_int),
110
+ ("font_size", ctypes.c_int),
111
+ ("serif", ctypes.c_int),
112
+ ("text", ctypes.c_char * 128),
113
+ ]
114
+
115
+
116
+ def _library_name() -> str:
117
+ if sys.platform == "win32":
118
+ return "libxppe.dll"
119
+ if sys.platform == "darwin":
120
+ return "libxppe.dylib"
121
+ return "libxppe.so"
122
+
123
+
124
+ def find_engine_library() -> Path:
125
+ """Locate the engine library.
126
+
127
+ The XPPLAB_ENGINE environment variable wins. Otherwise use the copy
128
+ installed inside the package, then a development build in the
129
+ repository's build/ directory.
130
+ """
131
+ env = os.environ.get("XPPLAB_ENGINE")
132
+ if env:
133
+ return Path(env)
134
+ here = Path(__file__).resolve().parent
135
+ candidates = [here / "_lib" / _library_name()]
136
+ repo = here.parents[2]
137
+ candidates += [repo / d / "engine" / _library_name()
138
+ for d in ("build", "build-release", "build-debug")]
139
+ for c in candidates:
140
+ if c.exists():
141
+ return c
142
+ raise EngineError(
143
+ "the XPP Lab engine library was not found; set XPPLAB_ENGINE "
144
+ "to the path of " + _library_name())
145
+
146
+
147
+ _SIGNATURES = {
148
+ "xppe_api_version": ([], ctypes.c_int),
149
+ "xppe_version": ([], ctypes.c_char_p),
150
+ "xppe_load": ([ctypes.c_char_p, ctypes.c_int,
151
+ ctypes.POINTER(ctypes.c_char_p)], ctypes.c_int),
152
+ "xppe_log": ([], ctypes.c_char_p),
153
+ "xppe_error": ([], ctypes.c_char_p),
154
+ "xppe_count": ([ctypes.c_int], ctypes.c_int),
155
+ "xppe_name": ([ctypes.c_int, ctypes.c_int], ctypes.c_char_p),
156
+ "xppe_get": ([ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)],
157
+ ctypes.c_int),
158
+ "xppe_set": ([ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int),
159
+ "xppe_set_number": ([ctypes.c_char_p, ctypes.c_double], ctypes.c_int),
160
+ "xppe_apply_set": ([ctypes.c_char_p], ctypes.c_int),
161
+ "xppe_run": ([], ctypes.c_int),
162
+ "xppe_rows": ([], ctypes.c_int),
163
+ "xppe_column": ([ctypes.c_int, ctypes.POINTER(ctypes.c_double),
164
+ ctypes.c_int], ctypes.c_int),
165
+ "xppe_equilibrium": ([ctypes.POINTER(ctypes.c_double)] * 3, ctypes.c_int),
166
+ "xppe_rhs": ([ctypes.c_double, ctypes.POINTER(ctypes.c_double),
167
+ ctypes.POINTER(ctypes.c_double)], ctypes.c_int),
168
+ "xppe_manifold_count": ([], ctypes.c_int),
169
+ "xppe_manifold": ([ctypes.c_int, ctypes.POINTER(ctypes.c_int)], ctypes.c_int),
170
+ "xppe_fourier": ([ctypes.c_char_p], ctypes.c_int),
171
+ "xppe_power": ([ctypes.c_char_p], ctypes.c_int),
172
+ "xppe_spectrum": ([ctypes.c_int, ctypes.c_char_p, ctypes.c_char_p,
173
+ ctypes.c_int, ctypes.c_int], ctypes.c_int),
174
+ "xppe_histogram": ([ctypes.c_char_p, ctypes.c_int, ctypes.c_double,
175
+ ctypes.c_double, ctypes.c_char_p], ctypes.c_int),
176
+ "xppe_histogram2d": ([ctypes.c_char_p, ctypes.c_int, ctypes.c_double,
177
+ ctypes.c_double, ctypes.c_char_p, ctypes.c_int,
178
+ ctypes.c_double, ctypes.c_double], ctypes.c_int),
179
+ "xppe_autocorrelation": ([ctypes.c_char_p, ctypes.c_int, ctypes.c_double,
180
+ ctypes.c_double], ctypes.c_int),
181
+ "xppe_correlation": ([ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int,
182
+ ctypes.c_int], ctypes.c_int),
183
+ "xppe_mean": ([ctypes.c_char_p, ctypes.POINTER(ctypes.c_double),
184
+ ctypes.POINTER(ctypes.c_double)], ctypes.c_int),
185
+ "xppe_lyapunov": ([ctypes.POINTER(ctypes.c_double)], ctypes.c_int),
186
+ "xppe_adjoint": ([ctypes.c_int, ctypes.c_double], ctypes.c_int),
187
+ "xppe_hfunction": ([ctypes.c_int, ctypes.POINTER(ctypes.c_char_p)],
188
+ ctypes.c_int),
189
+ "xppe_fit": ([ctypes.c_char_p, ctypes.c_int, ctypes.c_int, ctypes.c_char_p,
190
+ ctypes.c_char_p, ctypes.c_char_p, ctypes.c_double,
191
+ ctypes.c_double, ctypes.c_int], ctypes.c_int),
192
+ "xppe_result_rows": ([], ctypes.c_int),
193
+ "xppe_result_count": ([], ctypes.c_int),
194
+ "xppe_result_name": ([ctypes.c_int], ctypes.c_char_p),
195
+ "xppe_result_column": ([ctypes.c_int, ctypes.POINTER(ctypes.c_double),
196
+ ctypes.c_int], ctypes.c_int),
197
+ "xppe_ic_last": ([], ctypes.c_int),
198
+ "xppe_bvp_count": ([], ctypes.c_int),
199
+ "xppe_bvp": ([ctypes.c_int, ctypes.c_double, ctypes.c_double], ctypes.c_int),
200
+ "xppe_ani_load": ([ctypes.c_char_p], ctypes.c_int),
201
+ "xppe_ani_window": ([ctypes.POINTER(ctypes.c_double)] * 4, ctypes.c_int),
202
+ "xppe_ani_frame": ([ctypes.c_int], ctypes.c_int),
203
+ "xppe_ani_get": ([ctypes.c_int, ctypes.POINTER(AniItem)], ctypes.c_int),
204
+ "xppe_nullcline": ([ctypes.c_int, ctypes.c_int, ctypes.c_int,
205
+ ctypes.c_double, ctypes.c_double, ctypes.c_double,
206
+ ctypes.c_double, ctypes.c_int,
207
+ ctypes.POINTER(ctypes.c_float), ctypes.c_int],
208
+ ctypes.c_int),
209
+ "xppe_auto_get": ([ctypes.c_char_p, ctypes.POINTER(ctypes.c_double)],
210
+ ctypes.c_int),
211
+ "xppe_auto_set": ([ctypes.c_char_p, ctypes.c_double], ctypes.c_int),
212
+ "xppe_auto_parameters": ([ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int),
213
+ "xppe_auto_set_workdir": ([ctypes.c_char_p], ctypes.c_int),
214
+ "xppe_auto_reset": ([], ctypes.c_int),
215
+ "xppe_auto_run": ([ctypes.c_char_p], ctypes.c_int),
216
+ "xppe_auto_grab": ([ctypes.c_int], ctypes.c_int),
217
+ "xppe_auto_ungrab": ([], ctypes.c_int),
218
+ "xppe_auto_parameter_count": ([], ctypes.c_int),
219
+ "xppe_auto_parameter_name": ([ctypes.c_int], ctypes.c_char_p),
220
+ "xppe_auto_main_slot": ([ctypes.c_int], ctypes.c_int),
221
+ "xppe_auto_count": ([], ctypes.c_int),
222
+ "xppe_auto_point": ([ctypes.c_int, ctypes.POINTER(AutoPointInfo)],
223
+ ctypes.c_int),
224
+ "xppe_auto_point_data": ([ctypes.c_int, ctypes.c_int,
225
+ ctypes.POINTER(ctypes.c_double), ctypes.c_int],
226
+ ctypes.c_int),
227
+ }
228
+
229
+ # The engine reads and writes files relative to the working directory, and
230
+ # AUTO uses the HOME environment variable. Both are process-wide, so engine
231
+ # calls are serialised.
232
+ _process_lock = threading.RLock()
233
+
234
+
235
+ def _text(b: bytes | None) -> str:
236
+ return b.decode("latin-1") if b else ""
237
+
238
+
239
+ class Engine:
240
+ """One private copy of the engine library, holding at most one model."""
241
+
242
+ def __init__(self, library: str | os.PathLike | None = None):
243
+ source = Path(library) if library else find_engine_library()
244
+ self._tmpdir = Path(tempfile.mkdtemp(prefix="xpplab-"))
245
+ self._path = self._tmpdir / source.name
246
+ shutil.copy2(source, self._path)
247
+ self.workdir = self._tmpdir / "work"
248
+ self.workdir.mkdir()
249
+ self._lib = ctypes.CDLL(str(self._path))
250
+ for name, (args, res) in _SIGNATURES.items():
251
+ fn = getattr(self._lib, name)
252
+ fn.argtypes = args
253
+ fn.restype = res
254
+ if self._lib.xppe_api_version() != API_VERSION:
255
+ raise EngineError("engine library has an incompatible API version")
256
+ self.cwd: Path | None = None
257
+ self._closed = False
258
+ self._batch = 0
259
+
260
+ # -- plumbing ---------------------------------------------------------
261
+
262
+ @contextmanager
263
+ def batch(self):
264
+ """Hold the engine for many calls in a row (reading a diagram point by
265
+ point): one change of directory instead of two per call."""
266
+ with _process_lock:
267
+ old = os.getcwd()
268
+ if self.cwd is not None:
269
+ os.chdir(self.cwd)
270
+ self._batch += 1
271
+ try:
272
+ yield self
273
+ finally:
274
+ self._batch -= 1
275
+ os.chdir(old)
276
+
277
+ def call(self, name: str, *args, check: bool = True):
278
+ """Call an xppe_* function inside the model's working directory and
279
+ raise EngineError on failure."""
280
+ if self._closed:
281
+ raise EngineError("this model has been closed")
282
+ fn = getattr(self._lib, name)
283
+ with _process_lock:
284
+ if self._batch:
285
+ rc = fn(*args)
286
+ else:
287
+ old = os.getcwd()
288
+ if self.cwd is not None:
289
+ os.chdir(self.cwd)
290
+ try:
291
+ rc = fn(*args)
292
+ finally:
293
+ os.chdir(old)
294
+ if check and isinstance(rc, int) and rc < 0:
295
+ raise EngineError(_text(self._lib.xppe_error()) or name + " failed",
296
+ rc, self.log)
297
+ return rc
298
+
299
+ @property
300
+ def log(self) -> str:
301
+ return _text(self._lib.xppe_log())
302
+
303
+ @property
304
+ def version(self) -> str:
305
+ return _text(self._lib.xppe_version())
306
+
307
+ def names(self, kind: int) -> list[str]:
308
+ n = self._lib.xppe_count(kind)
309
+ return [_text(self._lib.xppe_name(kind, i)) for i in range(n)]
310
+
311
+ def close(self) -> None:
312
+ """Unload this copy of the engine and delete its files."""
313
+ if self._closed:
314
+ return
315
+ self._closed = True
316
+ handle = self._lib._handle
317
+ del self._lib
318
+ try:
319
+ if sys.platform == "win32":
320
+ ctypes.windll.kernel32.FreeLibrary(ctypes.c_void_p(handle))
321
+ else:
322
+ ctypes.CDLL(None).dlclose(ctypes.c_void_p(handle))
323
+ except Exception:
324
+ pass
325
+ shutil.rmtree(self._tmpdir, ignore_errors=True)
326
+
327
+ def __del__(self):
328
+ try:
329
+ self.close()
330
+ except Exception:
331
+ pass
Binary file