dynsim 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jens Egholm Pedersen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # dynsim
2
+
3
+ Interactive dynamical systems simulator for the browser. Define systems in Python (via [PyScript](https://pyscript.net/)), visualize with [Plotly](https://plotly.com/javascript/).
4
+
5
+ ## Quick start
6
+
7
+ ```html
8
+ <!-- 1. Load dependencies -->
9
+ <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
10
+ <script src="https://pyscript.net/releases/2024.11.1/core.js" type="module"></script>
11
+
12
+ <!-- 2. Load dynsim -->
13
+ <script src="https://unpkg.com/dynsim"></script>
14
+
15
+ <!-- 3. Add a container -->
16
+ <div id="my-system"></div>
17
+
18
+ <!-- 4. Define your system in Python (see code example below) -->
19
+ <script type="py" src="my-system.py"></script>
20
+ ```
21
+
22
+ ## Python code example
23
+
24
+ ```python
25
+ from pyscript import window
26
+ import json
27
+
28
+ def step(x, state, params):
29
+ """Damped harmonic oscillator: x'' + 2ζω₀x' + ω₀²x = u(t)"""
30
+ pos, vel, t = state["pos"], state["vel"], state["t"]
31
+ zeta, w0, dt = params["zeta"], params["w0"], params["dt"]
32
+
33
+ force = float(x)
34
+ acc = force - 2 * zeta * w0 * vel - w0**2 * pos
35
+ vel_new = vel + acc * dt
36
+ pos_new = pos + vel_new * dt
37
+
38
+ return [pos_new, {"pos": pos_new, "vel": vel_new, "t": t + dt}]
39
+
40
+ window.registerPythonSystem("my-system", step, {
41
+ "params": json.dumps([
42
+ {"id": "zeta", "label": "ζ", "min": 0, "max": 2, "step": 0.05, "value": 0.3},
43
+ {"id": "w0", "label": "ω₀", "min": 0.1, "max": 10, "step": 0.1, "value": 3.0},
44
+ ]),
45
+ "plotType": "timeseries",
46
+ "plotConfig": json.dumps({
47
+ "title": "Position",
48
+ "xaxis": {"title": "Time (s)", "range": [0, 20]},
49
+ "yaxis": {"title": "x", "range": [-3, 3]},
50
+ }),
51
+ "initialState": json.dumps({"pos": 0, "vel": 0, "t": 0}),
52
+ "initialX": 1,
53
+ "height": 450,
54
+ "dt": 0.02,
55
+ "pauseTime": 20, # pause at t=20 (omit for continuous)
56
+ })
57
+ ```
58
+
59
+ ## How it works
60
+
61
+ You write a Python `step(x, state, params)` function that takes:
62
+
63
+ | Argument | Description |
64
+ |----------|-------------|
65
+ | `x` | Current input value (from the slider) |
66
+ | `state` | Dictionary of internal state variables |
67
+ | `params` | Dictionary of parameter values (includes `dt`) |
68
+
69
+ It returns `[x_new, state_new]`. DynSim calls this every animation frame, feeds the result to Plotly, and gives the user sliders to control input and parameters in real time.
70
+
71
+ ## Features
72
+
73
+ - **Live code editing** — the `CodeEditor` component lets users modify the Python system definition on the page and apply changes without resetting the simulation.
74
+ - **Plot types** — `timeseries` (default), `3d` scatter, or `2d` phase plots.
75
+ - **Pause / reset** — built-in controls. Optional `pauseTime` to auto-pause at a given time.
76
+ - **Sliding time window** — for timeseries, the plot auto-scrolls as time advances.
77
+
78
+ ## Installation
79
+
80
+ **CDN (script tag):**
81
+
82
+ ```html
83
+ <script src="https://unpkg.com/dynsim"></script>
84
+ ```
85
+
86
+ **npm:**
87
+
88
+ ```bash
89
+ npm install dynsim
90
+ ```
91
+
92
+ ```js
93
+ import { Simulation, SimulationController, registry } from 'dynsim';
94
+ ```
95
+
96
+ ## Architecture
97
+
98
+ ```
99
+ Simulation — pure state + stepping logic (no DOM, testable in Node)
100
+ SimulationView — DOM creation, Plotly rendering, slider I/O
101
+ SimulationController — animation loop, wires Simulation ↔ View
102
+ CodeEditor — live Python code editor with hot-swap
103
+ registry — step function registry, supports live replacement
104
+ ```
105
+
106
+ ## Running the example locally
107
+
108
+ ```bash
109
+ npm install
110
+ npm run build
111
+ # Serve the examples directory (any static server works)
112
+ npx serve .
113
+ # Open http://localhost:3000/examples/damped-oscillator.html
114
+ ```
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ npm test # run tests
120
+ npm run test:watch # watch mode
121
+ npm run build # build ESM + UMD bundles to dist/
122
+ ```
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,81 @@
1
+ var C=Object.defineProperty;var P=(s,t)=>{for(var e in t)C(s,e,{get:t[e],enumerable:!0})};var p=class{constructor(t,e){this.params=t.params,this.plotType=t.plotType||"timeseries",this.plotConfig=t.plotConfig||{},this.initialState=t.initialState||{t:0},this.initialX=t.initialX??0,this.dt=t.dt||.01,this.maxPoints=t.maxPoints||1e3,this.pauseTime=t.pauseTime??null,this.spikes=t.spikes||null,this.stepProvider=e,this.x=this.initialX,this.state={...this.initialState},this.time=0,this.plotData=[],this.spikeTimes=[]}step(t,e){let i=this.stepProvider();if(!i)throw new Error("No step function available");let n={...e,dt:this.dt},o=i(t,this.state,n);this.x=o[0],this.state=o[1],this.time+=this.dt,this.spikes&&this.state[this.spikes]&&this.spikeTimes.push(this.time);let r=this._collectDataPoint();return this.plotData.push(r),this._manageBuffer(),{x:this.x,state:this.state,dataPoint:r}}get paused(){return this.pauseTime!=null&&this.time>=this.pauseTime}reset(){this.x=this.initialX,this.state={...this.initialState},this.time=0,this.plotData=[],this.spikeTimes=[]}getPlotArrays(){return this.plotType==="3d"?{x:this.plotData.map(t=>t[0]),y:this.plotData.map(t=>t[1]),z:this.plotData.map(t=>t[2])}:{x:this.plotData.map(t=>t[0]),y:this.plotData.map(t=>t[1])}}getTimeseriesRange(){let t=this.plotConfig.xaxis?.range?.[1]-this.plotConfig.xaxis?.range?.[0]||50,e=this.plotConfig.xaxis?.range?.[1]||50;return this.time>e?[this.time-t,this.time]:this.plotConfig.xaxis?.range||[0,50]}_collectDataPoint(){return this.plotType==="3d"?[this.state.x||this.x,this.state.y||0,this.state.z||0]:this.plotType==="timeseries"?[this.time,this.x]:[this.x,this.state.y||0]}_manageBuffer(){if(this.plotType==="timeseries"){let t=this.plotConfig.xaxis?.range?.[1]||50,e=Math.ceil(t/this.dt),i=Math.ceil(e*.5),n=e+i;if(this.plotData.length>n*2&&(this.plotData=this.plotData.slice(-n),this.spikeTimes.length>0)){let o=this.plotData[0][0],r=this.spikeTimes.findIndex(m=>m>=o);r>0&&(this.spikeTimes=this.spikeTimes.slice(r))}}else this.plotData.length>this.maxPoints&&this.plotData.shift()}};var a=class s{constructor({container:t,params:e,initialX:i,height:n,plotType:o,plotConfig:r,spikeThreshold:m,callbacks:v}){this.container=t,this.params=e,this.initialX=i,this.height=n||400,this.plotType=o||"timeseries",this.plotConfig=r||{},this.spikeThreshold=m,this.callbacks=v||{},this.plotDiv=null,this.createHTML(),this.initPlot()}createHTML(){this.container.innerHTML=`
2
+ <div class="dynsim-container" style="font-family: Arial, sans-serif; font-size: 0.9em;">
3
+ <div class="dynsim-controls" style="background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 12px; box-sizing: border-box;">
4
+ <div class="dynsim-params"></div>
5
+ </div>
6
+ <div style="width: 100%; height: ${this.height}px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; overflow: hidden;">
7
+ <div class="dynsim-plot" style="width: 100%; height: 100%;"></div>
8
+ </div>
9
+ </div>
10
+ `,this._buildControls(),this.plotDiv=this.container.querySelector(".dynsim-plot"),typeof MathJax<"u"&&MathJax.typesetPromise&&MathJax.typesetPromise([this.container.querySelector(".dynsim-controls")])}_buildControls(){let t=this.container.querySelector(".dynsim-params"),e=`
11
+ <div style="display: flex; gap: 12px; align-items: center; margin-bottom: 8px;">
12
+ <label style="font-weight: 600; font-size: 0.85em; color: #0056b3; white-space: nowrap;">Input (x):</label>
13
+ <input type="range" class="dynsim-input"
14
+ min="-2" max="2" step="0.1" value="${this.initialX}"
15
+ style="flex: 1; height: 6px; min-width: 100px;">
16
+ <span class="dynsim-input-value" style="background: #cfe2ff; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${this.initialX.toFixed(2)}</span>
17
+ <button class="dynsim-reset" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Reset">
18
+ <svg width="20" height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
19
+ <path d="M21 12a9 9 0 1 1-9 -9c2.5 0 4.8 1 6.5 2.5l.5 .5"/>
20
+ <path d="M21 3v6h-6"/>
21
+ </svg>
22
+ </button>
23
+ </div>
24
+ `;e+='<div style="display: flex; gap: 12px; align-items: center;">',e+=this.params.map(i=>`
25
+ <label style="font-weight: 600; font-size: 0.85em; white-space: nowrap;">${i.label}:</label>
26
+ <input type="range" class="dynsim-param" data-param="${i.id}"
27
+ min="${i.min}" max="${i.max}" step="${i.step}" value="${i.value}"
28
+ style="flex: 1; height: 6px; min-width: 100px;">
29
+ <span class="dynsim-param-value" style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${i.value.toFixed(2)}</span>
30
+ `).join(""),e+=`
31
+ <button class="dynsim-pause" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Pause">
32
+ ${s.PAUSE_ICON}
33
+ </button>
34
+ </div>`,t.innerHTML=e,t.querySelector(".dynsim-input").addEventListener("input",i=>{i.target.closest("div").querySelector(".dynsim-input-value").textContent=parseFloat(i.target.value).toFixed(2)}),t.querySelectorAll(".dynsim-param").forEach(i=>{i.addEventListener("input",n=>{n.target.nextElementSibling.textContent=parseFloat(n.target.value).toFixed(2)})}),this.container.querySelector(".dynsim-reset").addEventListener("click",()=>this.callbacks.onReset?.()),this.container.querySelector(".dynsim-pause").addEventListener("click",()=>this.callbacks.onPauseToggle?.())}initPlot(){if(this.plotType==="3d")Plotly.newPlot(this.plotDiv,[{x:[],y:[],z:[],mode:"lines",type:"scatter3d",line:{color:"#2196f3",width:4}}],{title:this.plotConfig.title,scene:{xaxis:{title:this.plotConfig.xaxis?.title||"X"},yaxis:{title:this.plotConfig.yaxis?.title||"Y"},zaxis:{title:this.plotConfig.zaxis?.title||"Z"}},margin:{l:0,r:0,t:30,b:0}});else{let t={margin:{l:50,r:20,t:40,b:50}};this.plotConfig.title&&(t.title=this.plotConfig.title),this.plotConfig.xaxis&&(t.xaxis=this.plotConfig.xaxis),this.plotConfig.yaxis&&(t.yaxis=this.plotConfig.yaxis),Plotly.newPlot(this.plotDiv,[{x:[],y:[],mode:"lines",line:{color:"#2196f3",width:2}}],t)}}getInput(){return parseFloat(this.container.querySelector(".dynsim-input").value)}getParameters(){let t={};return this.container.querySelectorAll(".dynsim-param").forEach(e=>{t[e.dataset.param]=parseFloat(e.value)}),t}updatePlot(t,e,i){if(this.plotType==="3d")Plotly.animate(this.plotDiv,{data:[{x:t.x,y:t.y,z:t.z}]},{transition:{duration:0},frame:{duration:0}});else if(this.plotType==="timeseries"){let n={title:this.plotConfig.title,xaxis:{title:this.plotConfig.xaxis?.title||"Time",range:e},yaxis:this.plotConfig.yaxis,margin:{l:50,r:20,t:40,b:50}},o=[];if(this.spikeThreshold!=null&&o.push({type:"line",x0:0,x1:1,xref:"paper",y0:this.spikeThreshold,y1:this.spikeThreshold,line:{color:"grey",width:1,dash:"dash"}}),i&&i.length>0)for(let r of i)o.push({type:"line",x0:r,x1:r,y0:0,y1:1,yref:"paper",line:{color:"rgba(255, 0, 0, 0.4)",width:1}});o.length>0&&(n.shapes=o),Plotly.react(this.plotDiv,[{x:t.x,y:t.y,mode:"lines",line:{color:"#2196f3",width:2}}],n)}else Plotly.animate(this.plotDiv,{data:[{x:t.x,y:t.y}]},{transition:{duration:0},frame:{duration:0}})}setPauseState(t){let e=this.container.querySelector(".dynsim-pause");e.title=t?"Pause":"Play",e.innerHTML=t?s.PAUSE_ICON:s.PLAY_ICON}destroy(){this.container.innerHTML=""}};a.PAUSE_ICON='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>';a.PLAY_ICON='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>';var h=class{constructor({container:t,config:e,stepProvider:i}){this.isRunning=!0,this.animationId=null,this.simulation=new p(e,i),this.view=new a({container:t,params:e.params,initialX:e.initialX??0,height:e.height||400,plotType:e.plotType||"timeseries",plotConfig:e.plotConfig||{},spikeThreshold:e.spikeThreshold??null,callbacks:{onReset:()=>this.reset(),onPauseToggle:()=>this.togglePause()}})}start(){this.animationId&&cancelAnimationFrame(this.animationId),this.animate()}stop(){this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null)}reset(){this.simulation.reset(),this.view.initPlot()}togglePause(){this.isRunning=!this.isRunning,this.isRunning&&this.simulation.paused&&(this.simulation.pauseTime=null),this.view.setPauseState(this.isRunning)}animate(){if(this.isRunning&&!this.simulation.paused){let n=this.view.getInput(),o=this.view.getParameters();try{this.simulation.step(n,o)}catch(r){console.error("[DynSim] Step error:",r),this.stop();return}this.simulation.paused&&(this.isRunning=!1,this.view.setPauseState(!1))}let t=this.simulation.getPlotArrays(),e=this.simulation.plotType==="timeseries"?this.simulation.getTimeseriesRange():void 0,i=this.simulation.spikes?this.simulation.spikeTimes:void 0;this.view.updatePlot(t,e,i),this.animationId=requestAnimationFrame(()=>this.animate())}destroy(){this.stop(),this.view.destroy()}};var c={};P(c,{getConfig:()=>d,getContainerIds:()=>f,getStep:()=>y,has:()=>k,register:()=>u,replaceStep:()=>_});var l={};function T(s){if(s==null)return s;if(typeof s.toJs=="function")try{return s.toJs({dict_converter:Object.fromEntries})}catch{try{return s.toJs()}catch{}}try{return JSON.parse(JSON.stringify(s))}catch{return s}}function u(s,t,e){let i=T(e);l[s]={step:t,config:{params:typeof i.params=="string"?JSON.parse(i.params):i.params||[],plotType:i.plotType||"timeseries",plotConfig:typeof i.plotConfig=="string"?JSON.parse(i.plotConfig):i.plotConfig||{},initialState:typeof i.initialState=="string"?JSON.parse(i.initialState):i.initialState||{t:0},initialX:i.initialX??0,height:i.height||400,dt:i.dt||.01,pauseTime:i.pauseTime??null,spikes:i.spikes||null,spikeThreshold:i.spikeThreshold??null}}}function y(s){return l[s]?.step||null}function d(s){return l[s]?.config||null}function _(s,t){l[s]&&(l[s].step=t)}function f(){return Object.keys(l)}function k(s){return s in l}var g=class{constructor({container:t,containerId:e,initialCode:i,executePython:n}){this.container=t,this.containerId=e,this.initialCode=i||"",this.executePython=n,this.textarea=null,this.statusEl=null,this.render()}render(){this.container.innerHTML=`
35
+ <div class="dynsim-editor" style="font-family: Arial, sans-serif; font-size: 0.9em; margin-bottom: 12px;">
36
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
37
+ <label style="font-weight: 600; font-size: 0.85em;">Python System Definition</label>
38
+ <div style="display: flex; gap: 8px; align-items: center;">
39
+ <span class="dynsim-editor-status" style="font-size: 0.8em; color: #666;"></span>
40
+ <button class="dynsim-editor-apply" style="
41
+ background: #0056b3; color: white; border: none; border-radius: 4px;
42
+ padding: 4px 12px; cursor: pointer; font-size: 0.85em;
43
+ ">Apply</button>
44
+ <button class="dynsim-editor-reset" style="
45
+ background: #6c757d; color: white; border: none; border-radius: 4px;
46
+ padding: 4px 12px; cursor: pointer; font-size: 0.85em;
47
+ ">Reset Code</button>
48
+ </div>
49
+ </div>
50
+ <textarea class="dynsim-editor-textarea" style="
51
+ width: 100%; min-height: 200px; font-family: monospace; font-size: 0.9em;
52
+ padding: 8px; border: 1px solid #ddd; border-radius: 6px;
53
+ box-sizing: border-box; resize: vertical; tab-size: 4;
54
+ " spellcheck="false">${this._escapeHtml(this.initialCode)}</textarea>
55
+ </div>
56
+ `,this.textarea=this.container.querySelector(".dynsim-editor-textarea"),this.statusEl=this.container.querySelector(".dynsim-editor-status"),this.container.querySelector(".dynsim-editor-apply").addEventListener("click",()=>this.apply()),this.container.querySelector(".dynsim-editor-reset").addEventListener("click",()=>this.resetCode()),this.textarea.addEventListener("keydown",t=>{if(t.key==="Tab"){t.preventDefault();let e=this.textarea.selectionStart,i=this.textarea.selectionEnd;this.textarea.value=this.textarea.value.substring(0,e)+" "+this.textarea.value.substring(i),this.textarea.selectionStart=this.textarea.selectionEnd=e+4}})}apply(){let t=this.textarea.value,e=d(this.containerId);try{this.executePython(t,this.containerId,e),this._setStatus("Applied","green")}catch(i){console.error("[DynSim Editor] Error applying code:",i),this._setStatus("Error: "+i.message,"red")}}resetCode(){this.textarea.value=this.initialCode,this._setStatus("Reset to original","#666")}getCode(){return this.textarea.value}_setStatus(t,e){this.statusEl.textContent=t,this.statusEl.style.color=e,setTimeout(()=>{this.statusEl.textContent===t&&(this.statusEl.textContent="")},3e3)}_escapeHtml(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}};var D=`
57
+ def _dynsim_init_bridge():
58
+ from pyscript import window as _w
59
+ from pyodide.ffi import create_proxy, to_js
60
+ from js import Object
61
+
62
+ _js_register = _w._dynsimJsRegister
63
+
64
+ def _register(container_id, step_fn, config):
65
+ def wrapped_step(x, state, params):
66
+ # Convert JS inputs to Python dicts
67
+ s = state.to_py() if hasattr(state, 'to_py') else state
68
+ p = params.to_py() if hasattr(params, 'to_py') else params
69
+ result = step_fn(float(x), s, p)
70
+ # Convert Python outputs to plain JS objects
71
+ return to_js(result, dict_converter=Object.fromEntries)
72
+ # create_proxy prevents the wrapped function from being garbage collected
73
+ # after this call returns \u2014 it's called repeatedly from requestAnimationFrame
74
+ _js_register(container_id, create_proxy(wrapped_step), config)
75
+
76
+ _w.registerPythonSystem = _register
77
+
78
+ _dynsim_init_bridge()
79
+ del _dynsim_init_bridge
80
+ `;function E(){let s=document.createElement("script");s.type="py",s.textContent=D,document.head.insertBefore(s,document.head.firstChild)}function z(s){if(s!=null&&typeof s.toJs=="function")try{return s.toJs({dict_converter:Object.fromEntries})}catch{try{return s.toJs()}catch{}}return s}async function F(){window.pythonSystems={},window.dynSimConfigs={},window._dynsimJsRegister=function(s,t,e){console.log("[DynSim] Registering Python system:",s),u(s,t,e),document.readyState==="complete"&&typeof Plotly<"u"&&S(s)},window.registerPythonSystem||(window.registerPythonSystem=window._dynsimJsRegister),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",async()=>{await b(),w()}):(await b(),w())}var x={};function S(s){let t=document.getElementById(s);if(!t||t.querySelector(".dynsim-container"))return;let e=d(s);e&&(x[s]=new h({container:t,config:e,stepProvider:()=>y(s)}),x[s].start())}function w(){let s=0,t=20;function e(){if(s++,typeof Plotly>"u"){s<t&&setTimeout(e,50);return}let i=f();if(i.length===0&&s<t){setTimeout(e,50);return}i.forEach(S)}e()}async function b(){let s=0;for(;!window.dynSimSystemsData&&s<20;)await new Promise(e=>setTimeout(e,500)),s++;if(!window.dynSimSystemsData)return;for(s=0;!window.executeDynSimCode&&s<40;)await new Promise(e=>setTimeout(e,500)),s++;if(!window.executeDynSimCode)return;let t=document.querySelectorAll(".dynsim-python-container");for(let e of t){let i=window.dynSimSystemsData[e.id];if(i)try{window.dynSimConfigs[e.id]=i.config,window.executeDynSimCode(i.pythonCode,e.id,i.config)}catch(n){console.error("[DynSim] Error processing container:",e.id,n)}}}export{g as CodeEditor,p as Simulation,h as SimulationController,a as SimulationView,F as autoInit,E as injectPythonBridge,z as pyToJs,c as registry};
81
+ //# sourceMappingURL=dynsim.esm.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/simulation.js", "../src/view.js", "../src/controller.js", "../src/registry.js", "../src/editor.js", "../src/pybridge.js", "../src/index.js"],
4
+ "sourcesContent": ["/**\n * Pure simulation engine \u2014 no DOM, no Plotly.\n *\n * Manages state, stepping, and plot data buffering.\n * The step function is resolved via a stepProvider on each tick,\n * enabling live code replacement.\n */\nexport class Simulation {\n /**\n * @param {object} config\n * @param {Array} config.params - Parameter definitions [{id, label, min, max, step, value}]\n * @param {string} config.plotType - 'timeseries' | '3d' | '2d'\n * @param {object} config.plotConfig - Plotly layout config (title, axes, etc.)\n * @param {object} config.initialState - Initial state object\n * @param {number} config.initialX - Initial input/output value\n * @param {number} config.dt - Time step\n * @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)\n * @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)\n * @param {number} [config.pauseTime=null] - Pause simulation at this time (null = run forever)\n * @param {string} [config.spikes=null] - State variable name to check for spikes (e.g. 'z'). Falsy = no spikes.\n * @param {function} stepProvider - () => stepFunction. Called each tick to get the current step function.\n */\n constructor(config, stepProvider) {\n this.params = config.params;\n this.plotType = config.plotType || 'timeseries';\n this.plotConfig = config.plotConfig || {};\n this.initialState = config.initialState || { t: 0 };\n this.initialX = config.initialX ?? 0;\n this.dt = config.dt || 0.01;\n this.maxPoints = config.maxPoints || 1000;\n this.pauseTime = config.pauseTime ?? null;\n this.spikes = config.spikes || null;\n\n this.stepProvider = stepProvider;\n\n this.x = this.initialX;\n this.state = { ...this.initialState };\n this.time = 0;\n this.plotData = [];\n this.spikeTimes = [];\n }\n\n /**\n * Advance one time step.\n * @param {number} inputValue - Current input from the user (slider)\n * @param {object} paramValues - Parameter values keyed by param ID\n * @returns {{ x: number, state: object, dataPoint: Array }} result of the step\n * @throws if stepProvider returns null or step function throws\n */\n step(inputValue, paramValues) {\n const stepFn = this.stepProvider();\n if (!stepFn) {\n throw new Error('No step function available');\n }\n\n // Add dt to params\n const params = { ...paramValues, dt: this.dt };\n\n const result = stepFn(inputValue, this.state, params);\n this.x = result[0];\n this.state = result[1];\n this.time += this.dt;\n\n // Record spike if the configured state variable is truthy\n if (this.spikes && this.state[this.spikes]) {\n this.spikeTimes.push(this.time);\n }\n\n const dataPoint = this._collectDataPoint();\n this.plotData.push(dataPoint);\n this._manageBuffer();\n\n return { x: this.x, state: this.state, dataPoint };\n }\n\n /**\n * Whether the simulation has reached its pause time.\n * @returns {boolean}\n */\n get paused() {\n return this.pauseTime != null && this.time >= this.pauseTime;\n }\n\n /**\n * Reset simulation to initial conditions.\n */\n reset() {\n this.x = this.initialX;\n this.state = { ...this.initialState };\n this.time = 0;\n this.plotData = [];\n this.spikeTimes = [];\n }\n\n /**\n * Get current plot data arrays suitable for Plotly.\n * @returns {object} { x, y, z? } arrays\n */\n getPlotArrays() {\n if (this.plotType === '3d') {\n return {\n x: this.plotData.map(d => d[0]),\n y: this.plotData.map(d => d[1]),\n z: this.plotData.map(d => d[2])\n };\n }\n return {\n x: this.plotData.map(d => d[0]),\n y: this.plotData.map(d => d[1])\n };\n }\n\n /**\n * Compute the current x-axis range for timeseries plots.\n * @returns {[number, number]} [min, max]\n */\n getTimeseriesRange() {\n const windowSize = (this.plotConfig.xaxis?.range?.[1] - this.plotConfig.xaxis?.range?.[0]) || 50;\n const originalEnd = this.plotConfig.xaxis?.range?.[1] || 50;\n\n if (this.time > originalEnd) {\n return [this.time - windowSize, this.time];\n }\n return this.plotConfig.xaxis?.range || [0, 50];\n }\n\n // --- Private ---\n\n _collectDataPoint() {\n if (this.plotType === '3d') {\n return [this.state.x || this.x, this.state.y || 0, this.state.z || 0];\n } else if (this.plotType === 'timeseries') {\n return [this.time, this.x];\n } else {\n return [this.x, this.state.y || 0];\n }\n }\n\n _manageBuffer() {\n if (this.plotType === 'timeseries') {\n const windowSize = this.plotConfig.xaxis?.range?.[1] || 50;\n const pointsPerWindow = Math.ceil(windowSize / this.dt);\n const bufferPoints = Math.ceil(pointsPerWindow * 0.5);\n const targetPoints = pointsPerWindow + bufferPoints;\n\n if (this.plotData.length > targetPoints * 2) {\n this.plotData = this.plotData.slice(-targetPoints);\n // Trim old spike times outside the buffer\n if (this.spikeTimes.length > 0) {\n const cutoff = this.plotData[0][0]; // earliest time in buffer\n const firstKeep = this.spikeTimes.findIndex(t => t >= cutoff);\n if (firstKeep > 0) this.spikeTimes = this.spikeTimes.slice(firstKeep);\n }\n }\n } else {\n if (this.plotData.length > this.maxPoints) {\n this.plotData.shift();\n }\n }\n }\n}\n", "/**\n * SimulationView \u2014 DOM + Plotly rendering.\n *\n * Creates the UI (sliders, buttons, plot area), reads user input,\n * and updates the Plotly chart. Emits events via callbacks.\n * Contains no simulation logic.\n */\nexport class SimulationView {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element to render into\n * @param {Array} options.params - Parameter definitions [{id, label, min, max, step, value}]\n * @param {number} options.initialX - Initial input value\n * @param {number} options.height - Plot height in pixels\n * @param {string} options.plotType - 'timeseries' | '3d' | '2d'\n * @param {object} options.plotConfig - Plotly layout config\n * @param {object} [options.callbacks] - { onReset, onPauseToggle }\n */\n constructor({ container, params, initialX, height, plotType, plotConfig, spikeThreshold, callbacks }) {\n this.container = container;\n this.params = params;\n this.initialX = initialX;\n this.height = height || 400;\n this.plotType = plotType || 'timeseries';\n this.plotConfig = plotConfig || {};\n this.spikeThreshold = spikeThreshold;\n this.callbacks = callbacks || {};\n\n this.plotDiv = null;\n\n this.createHTML();\n this.initPlot();\n }\n\n createHTML() {\n this.container.innerHTML = `\n <div class=\"dynsim-container\" style=\"font-family: Arial, sans-serif; font-size: 0.9em;\">\n <div class=\"dynsim-controls\" style=\"background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 12px; box-sizing: border-box;\">\n <div class=\"dynsim-params\"></div>\n </div>\n <div style=\"width: 100%; height: ${this.height}px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; overflow: hidden;\">\n <div class=\"dynsim-plot\" style=\"width: 100%; height: 100%;\"></div>\n </div>\n </div>\n `;\n\n this._buildControls();\n this.plotDiv = this.container.querySelector('.dynsim-plot');\n\n // Typeset LaTeX in labels if MathJax is available\n if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {\n MathJax.typesetPromise([this.container.querySelector('.dynsim-controls')]);\n }\n }\n\n _buildControls() {\n const paramsDiv = this.container.querySelector('.dynsim-params');\n\n // Input slider row\n let html = `\n <div style=\"display: flex; gap: 12px; align-items: center; margin-bottom: 8px;\">\n <label style=\"font-weight: 600; font-size: 0.85em; color: #0056b3; white-space: nowrap;\">Input (x):</label>\n <input type=\"range\" class=\"dynsim-input\"\n min=\"-2\" max=\"2\" step=\"0.1\" value=\"${this.initialX}\"\n style=\"flex: 1; height: 6px; min-width: 100px;\">\n <span class=\"dynsim-input-value\" style=\"background: #cfe2ff; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;\">${this.initialX.toFixed(2)}</span>\n <button class=\"dynsim-reset\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;\" title=\"Reset\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-9 -9c2.5 0 4.8 1 6.5 2.5l.5 .5\"/>\n <path d=\"M21 3v6h-6\"/>\n </svg>\n </button>\n </div>\n `;\n\n // Parameter sliders row\n html += `<div style=\"display: flex; gap: 12px; align-items: center;\">`;\n html += this.params.map(param => `\n <label style=\"font-weight: 600; font-size: 0.85em; white-space: nowrap;\">${param.label}:</label>\n <input type=\"range\" class=\"dynsim-param\" data-param=\"${param.id}\"\n min=\"${param.min}\" max=\"${param.max}\" step=\"${param.step}\" value=\"${param.value}\"\n style=\"flex: 1; height: 6px; min-width: 100px;\">\n <span class=\"dynsim-param-value\" style=\"background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;\">${param.value.toFixed(2)}</span>\n `).join('');\n html += `\n <button class=\"dynsim-pause\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;\" title=\"Pause\">\n ${SimulationView.PAUSE_ICON}\n </button>\n </div>`;\n\n paramsDiv.innerHTML = html;\n\n // Wire up slider display updates\n paramsDiv.querySelector('.dynsim-input').addEventListener('input', (e) => {\n e.target.closest('div').querySelector('.dynsim-input-value')\n .textContent = parseFloat(e.target.value).toFixed(2);\n });\n paramsDiv.querySelectorAll('.dynsim-param').forEach(slider => {\n slider.addEventListener('input', (e) => {\n e.target.nextElementSibling.textContent = parseFloat(e.target.value).toFixed(2);\n });\n });\n\n // Wire up button callbacks\n this.container.querySelector('.dynsim-reset')\n .addEventListener('click', () => this.callbacks.onReset?.());\n this.container.querySelector('.dynsim-pause')\n .addEventListener('click', () => this.callbacks.onPauseToggle?.());\n }\n\n initPlot() {\n if (this.plotType === '3d') {\n Plotly.newPlot(this.plotDiv, [{\n x: [], y: [], z: [],\n mode: 'lines',\n type: 'scatter3d',\n line: { color: '#2196f3', width: 4 }\n }], {\n title: this.plotConfig.title,\n scene: {\n xaxis: { title: this.plotConfig.xaxis?.title || 'X' },\n yaxis: { title: this.plotConfig.yaxis?.title || 'Y' },\n zaxis: { title: this.plotConfig.zaxis?.title || 'Z' }\n },\n margin: { l: 0, r: 0, t: 30, b: 0 }\n });\n } else {\n const layout = { margin: { l: 50, r: 20, t: 40, b: 50 } };\n if (this.plotConfig.title) layout.title = this.plotConfig.title;\n if (this.plotConfig.xaxis) layout.xaxis = this.plotConfig.xaxis;\n if (this.plotConfig.yaxis) layout.yaxis = this.plotConfig.yaxis;\n\n Plotly.newPlot(this.plotDiv, [{\n x: [], y: [],\n mode: 'lines',\n line: { color: '#2196f3', width: 2 }\n }], layout);\n }\n }\n\n /**\n * Read the current input slider value.\n * @returns {number}\n */\n getInput() {\n return parseFloat(this.container.querySelector('.dynsim-input').value);\n }\n\n /**\n * Read current parameter slider values as a dict keyed by param ID.\n * @returns {object}\n */\n getParameters() {\n const result = {};\n this.container.querySelectorAll('.dynsim-param').forEach(slider => {\n result[slider.dataset.param] = parseFloat(slider.value);\n });\n return result;\n }\n\n /**\n * Update the Plotly chart with new data.\n * @param {object} plotArrays - { x, y, z? } from Simulation.getPlotArrays()\n * @param {[number, number]} [xRange] - x-axis range for timeseries\n * @param {number[]} [spikeTimes] - spike times to render as vertical lines\n */\n updatePlot(plotArrays, xRange, spikeTimes) {\n if (this.plotType === '3d') {\n Plotly.animate(this.plotDiv, {\n data: [{ x: plotArrays.x, y: plotArrays.y, z: plotArrays.z }]\n }, { transition: { duration: 0 }, frame: { duration: 0 } });\n } else if (this.plotType === 'timeseries') {\n const layout = {\n title: this.plotConfig.title,\n xaxis: { title: this.plotConfig.xaxis?.title || 'Time', range: xRange },\n yaxis: this.plotConfig.yaxis,\n margin: { l: 50, r: 20, t: 40, b: 50 }\n };\n\n // Render spike markers and threshold line\n const shapes = [];\n\n // Spike threshold: horizontal dashed line\n if (this.spikeThreshold != null) {\n shapes.push({\n type: 'line',\n x0: 0, x1: 1, xref: 'paper',\n y0: this.spikeThreshold, y1: this.spikeThreshold,\n line: { color: 'grey', width: 1, dash: 'dash' }\n });\n }\n\n // Spike times: vertical lines\n if (spikeTimes && spikeTimes.length > 0) {\n for (const t of spikeTimes) {\n shapes.push({\n type: 'line',\n x0: t, x1: t,\n y0: 0, y1: 1, yref: 'paper',\n line: { color: 'rgba(255, 0, 0, 0.4)', width: 1 }\n });\n }\n }\n\n if (shapes.length > 0) layout.shapes = shapes;\n\n Plotly.react(this.plotDiv,\n [{ x: plotArrays.x, y: plotArrays.y, mode: 'lines', line: { color: '#2196f3', width: 2 } }],\n layout\n );\n } else {\n Plotly.animate(this.plotDiv, {\n data: [{ x: plotArrays.x, y: plotArrays.y }]\n }, { transition: { duration: 0 }, frame: { duration: 0 } });\n }\n }\n\n /**\n * Update the pause button icon.\n * @param {boolean} isRunning\n */\n setPauseState(isRunning) {\n const btn = this.container.querySelector('.dynsim-pause');\n btn.title = isRunning ? 'Pause' : 'Play';\n btn.innerHTML = isRunning ? SimulationView.PAUSE_ICON : SimulationView.PLAY_ICON;\n }\n\n destroy() {\n this.container.innerHTML = '';\n }\n}\n\nSimulationView.PAUSE_ICON = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><line x1=\"10\" y1=\"15\" x2=\"10\" y2=\"9\"></line><line x1=\"14\" y1=\"15\" x2=\"14\" y2=\"9\"></line></svg>`;\n\nSimulationView.PLAY_ICON = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polygon points=\"10 8 16 12 10 16 10 8\"></polygon></svg>`;\n", "/**\n * SimulationController \u2014 wires Simulation + SimulationView.\n *\n * Owns the requestAnimationFrame loop. Each tick:\n * reads input from view \u2192 calls simulation.step() \u2192 updates view.\n */\nimport { Simulation } from './simulation.js';\nimport { SimulationView } from './view.js';\n\nexport class SimulationController {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element for the simulation view\n * @param {object} options.config - Parsed system config (from registry)\n * @param {function} options.stepProvider - () => stepFunction\n */\n constructor({ container, config, stepProvider }) {\n this.isRunning = true;\n this.animationId = null;\n\n this.simulation = new Simulation(config, stepProvider);\n\n this.view = new SimulationView({\n container,\n params: config.params,\n initialX: config.initialX ?? 0,\n height: config.height || 400,\n plotType: config.plotType || 'timeseries',\n plotConfig: config.plotConfig || {},\n spikeThreshold: config.spikeThreshold ?? null,\n callbacks: {\n onReset: () => this.reset(),\n onPauseToggle: () => this.togglePause()\n }\n });\n }\n\n start() {\n if (this.animationId) {\n cancelAnimationFrame(this.animationId);\n }\n this.animate();\n }\n\n stop() {\n if (this.animationId) {\n cancelAnimationFrame(this.animationId);\n this.animationId = null;\n }\n }\n\n reset() {\n this.simulation.reset();\n this.view.initPlot();\n }\n\n togglePause() {\n this.isRunning = !this.isRunning;\n // Clear pause time so the simulation can continue past it\n if (this.isRunning && this.simulation.paused) {\n this.simulation.pauseTime = null;\n }\n this.view.setPauseState(this.isRunning);\n }\n\n animate() {\n if (this.isRunning && !this.simulation.paused) {\n const inputValue = this.view.getInput();\n const paramValues = this.view.getParameters();\n\n try {\n this.simulation.step(inputValue, paramValues);\n } catch (e) {\n console.error('[DynSim] Step error:', e);\n this.stop();\n return;\n }\n\n // Auto-pause when pause time is reached\n if (this.simulation.paused) {\n this.isRunning = false;\n this.view.setPauseState(false);\n }\n }\n\n const plotArrays = this.simulation.getPlotArrays();\n const xRange = this.simulation.plotType === 'timeseries'\n ? this.simulation.getTimeseriesRange()\n : undefined;\n const spikeTimes = this.simulation.spikes ? this.simulation.spikeTimes : undefined;\n this.view.updatePlot(plotArrays, xRange, spikeTimes);\n\n this.animationId = requestAnimationFrame(() => this.animate());\n }\n\n destroy() {\n this.stop();\n this.view.destroy();\n }\n}\n", "/**\n * Registry for step functions, keyed by container ID.\n *\n * Supports live replacement: update a step function at any time\n * and the next simulation tick will pick it up via the stepProvider pattern.\n */\n\nconst systems = {};\n\n/**\n * Ensure a value is a plain JS object (not a PyProxy or other foreign wrapper).\n * Uses Pyodide's toJs() if available, otherwise falls back to JSON round-trip.\n */\nfunction toPlainObject(value) {\n if (value == null) return value;\n // Pyodide PyProxy \u2014 use toJs with dict_converter for proper dict\u2192object conversion\n if (typeof value.toJs === 'function') {\n try {\n return value.toJs({ dict_converter: Object.fromEntries });\n } catch {\n try { return value.toJs(); } catch {}\n }\n }\n // Fallback: JSON round-trip (works for plain JS objects)\n try {\n return JSON.parse(JSON.stringify(value));\n } catch {\n return value;\n }\n}\n\n/**\n * Register (or replace) a step function and config for a container.\n * @param {string} containerId\n * @param {function} stepFunction - step(x, state, params) => [x_new, state_new]\n * @param {object} rawConfig - Config object (plain JS or Python dict proxy \u2014 converted implicitly)\n */\nexport function register(containerId, stepFunction, rawConfig) {\n // Convert potential PyProxy to plain JS object\n const cfg = toPlainObject(rawConfig);\n systems[containerId] = {\n step: stepFunction,\n config: {\n params: typeof cfg.params === 'string' ? JSON.parse(cfg.params) : (cfg.params || []),\n plotType: cfg.plotType || 'timeseries',\n plotConfig: typeof cfg.plotConfig === 'string' ? JSON.parse(cfg.plotConfig) : (cfg.plotConfig || {}),\n initialState: typeof cfg.initialState === 'string' ? JSON.parse(cfg.initialState) : (cfg.initialState || { t: 0 }),\n initialX: cfg.initialX ?? 0,\n height: cfg.height || 400,\n dt: cfg.dt || 0.01,\n pauseTime: cfg.pauseTime ?? null,\n spikes: cfg.spikes || null,\n spikeThreshold: cfg.spikeThreshold ?? null\n }\n };\n}\n\n/**\n * Get the current step function for a container.\n * @param {string} containerId\n * @returns {function|null}\n */\nexport function getStep(containerId) {\n return systems[containerId]?.step || null;\n}\n\n/**\n * Get the parsed config for a container.\n * @param {string} containerId\n * @returns {object|null}\n */\nexport function getConfig(containerId) {\n return systems[containerId]?.config || null;\n}\n\n/**\n * Replace just the step function (for live code editing).\n * @param {string} containerId\n * @param {function} stepFunction\n */\nexport function replaceStep(containerId, stepFunction) {\n if (systems[containerId]) {\n systems[containerId].step = stepFunction;\n }\n}\n\n/**\n * Get all registered container IDs.\n * @returns {string[]}\n */\nexport function getContainerIds() {\n return Object.keys(systems);\n}\n\n/**\n * Check if a container is registered.\n * @param {string} containerId\n * @returns {boolean}\n */\nexport function has(containerId) {\n return containerId in systems;\n}\n", "/**\n * CodeEditor \u2014 in-page Python code editor with live replacement.\n *\n * Provides a textarea for editing the Python step function definition.\n * On \"Apply\", re-executes the code via PyScript/Pyodide and updates\n * the step function in the registry. The simulation continues\n * seamlessly with the new dynamics on the next tick.\n */\nimport * as registry from './registry.js';\n\nexport class CodeEditor {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element to render the editor into\n * @param {string} options.containerId - The simulation container ID (registry key)\n * @param {string} options.initialCode - Initial Python source code\n * @param {function} options.executePython - (code, containerId, config) => void\n * Function that executes Python code in the PyScript/Pyodide runtime.\n * The code should call registerPythonSystem which updates the registry.\n */\n constructor({ container, containerId, initialCode, executePython }) {\n this.container = container;\n this.containerId = containerId;\n this.initialCode = initialCode || '';\n this.executePython = executePython;\n this.textarea = null;\n this.statusEl = null;\n\n this.render();\n }\n\n render() {\n this.container.innerHTML = `\n <div class=\"dynsim-editor\" style=\"font-family: Arial, sans-serif; font-size: 0.9em; margin-bottom: 12px;\">\n <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;\">\n <label style=\"font-weight: 600; font-size: 0.85em;\">Python System Definition</label>\n <div style=\"display: flex; gap: 8px; align-items: center;\">\n <span class=\"dynsim-editor-status\" style=\"font-size: 0.8em; color: #666;\"></span>\n <button class=\"dynsim-editor-apply\" style=\"\n background: #0056b3; color: white; border: none; border-radius: 4px;\n padding: 4px 12px; cursor: pointer; font-size: 0.85em;\n \">Apply</button>\n <button class=\"dynsim-editor-reset\" style=\"\n background: #6c757d; color: white; border: none; border-radius: 4px;\n padding: 4px 12px; cursor: pointer; font-size: 0.85em;\n \">Reset Code</button>\n </div>\n </div>\n <textarea class=\"dynsim-editor-textarea\" style=\"\n width: 100%; min-height: 200px; font-family: monospace; font-size: 0.9em;\n padding: 8px; border: 1px solid #ddd; border-radius: 6px;\n box-sizing: border-box; resize: vertical; tab-size: 4;\n \" spellcheck=\"false\">${this._escapeHtml(this.initialCode)}</textarea>\n </div>\n `;\n\n this.textarea = this.container.querySelector('.dynsim-editor-textarea');\n this.statusEl = this.container.querySelector('.dynsim-editor-status');\n\n this.container.querySelector('.dynsim-editor-apply')\n .addEventListener('click', () => this.apply());\n\n this.container.querySelector('.dynsim-editor-reset')\n .addEventListener('click', () => this.resetCode());\n\n // Tab key inserts spaces instead of changing focus\n this.textarea.addEventListener('keydown', (e) => {\n if (e.key === 'Tab') {\n e.preventDefault();\n const start = this.textarea.selectionStart;\n const end = this.textarea.selectionEnd;\n this.textarea.value =\n this.textarea.value.substring(0, start) +\n ' ' +\n this.textarea.value.substring(end);\n this.textarea.selectionStart = this.textarea.selectionEnd = start + 4;\n }\n });\n }\n\n /**\n * Re-execute the current code and update the registry.\n */\n apply() {\n const code = this.textarea.value;\n const config = registry.getConfig(this.containerId);\n\n try {\n this.executePython(code, this.containerId, config);\n this._setStatus('Applied', 'green');\n } catch (e) {\n console.error('[DynSim Editor] Error applying code:', e);\n this._setStatus('Error: ' + e.message, 'red');\n }\n }\n\n /**\n * Reset the textarea to the initial code.\n */\n resetCode() {\n this.textarea.value = this.initialCode;\n this._setStatus('Reset to original', '#666');\n }\n\n /**\n * Get the current code from the editor.\n * @returns {string}\n */\n getCode() {\n return this.textarea.value;\n }\n\n _setStatus(text, color) {\n this.statusEl.textContent = text;\n this.statusEl.style.color = color;\n // Clear status after 3 seconds\n setTimeout(() => {\n if (this.statusEl.textContent === text) {\n this.statusEl.textContent = '';\n }\n }, 3000);\n }\n\n _escapeHtml(str) {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n}\n", "/**\n * PyBridge \u2014 transparent type conversion between JS and Python (Pyodide).\n *\n * Injects a Python-side bridge script that wraps step functions so that\n * JS objects are automatically converted to Python dicts before the user's\n * step function is called. Neither side needs to know about the other.\n *\n * Flow:\n * 1. UMD load \u2192 injectPythonBridge() inserts <script type=\"py\"> into DOM\n * 2. PyScript processes it before user scripts (document order)\n * 3. Python bridge redefines window.registerPythonSystem to wrap step functions\n * 4. User's Python code calls registerPythonSystem as normal\n * 5. The wrapper converts JsProxy args to Python dicts, calls real step,\n * and returns the result\n */\n\nconst PYTHON_BRIDGE = `\ndef _dynsim_init_bridge():\n from pyscript import window as _w\n from pyodide.ffi import create_proxy, to_js\n from js import Object\n\n _js_register = _w._dynsimJsRegister\n\n def _register(container_id, step_fn, config):\n def wrapped_step(x, state, params):\n # Convert JS inputs to Python dicts\n s = state.to_py() if hasattr(state, 'to_py') else state\n p = params.to_py() if hasattr(params, 'to_py') else params\n result = step_fn(float(x), s, p)\n # Convert Python outputs to plain JS objects\n return to_js(result, dict_converter=Object.fromEntries)\n # create_proxy prevents the wrapped function from being garbage collected\n # after this call returns \u2014 it's called repeatedly from requestAnimationFrame\n _js_register(container_id, create_proxy(wrapped_step), config)\n\n _w.registerPythonSystem = _register\n\n_dynsim_init_bridge()\ndel _dynsim_init_bridge\n`;\n\n/**\n * Inject the Python bridge script into the DOM.\n * Must be called synchronously during UMD script load (before PyScript processes scripts).\n */\nexport function injectPythonBridge() {\n const script = document.createElement('script');\n script.type = 'py';\n script.textContent = PYTHON_BRIDGE;\n // Insert as first child of <head> so it runs before user's <script type=\"py\"> tags\n document.head.insertBefore(script, document.head.firstChild);\n}\n\n/**\n * Convert a Python proxy value to a plain JS value.\n */\nexport function pyToJs(value) {\n if (value != null && typeof value.toJs === 'function') {\n try {\n return value.toJs({ dict_converter: Object.fromEntries });\n } catch {\n try { return value.toJs(); } catch {}\n }\n }\n return value;\n}\n", "/**\n * dynsim \u2014 Interactive dynamical systems simulator.\n *\n * ES module entry point. Exports all public API.\n */\nexport { Simulation } from './simulation.js';\nexport { SimulationView } from './view.js';\nexport { SimulationController } from './controller.js';\nexport { CodeEditor } from './editor.js';\nexport * as registry from './registry.js';\nexport { pyToJs, injectPythonBridge } from './pybridge.js';\n\nimport { SimulationController } from './controller.js';\nimport * as registry from './registry.js';\n\n/**\n * Auto-initialize: expose globals for PyScript interop and\n * bootstrap PyScript containers. Called automatically by the UMD build.\n * Call manually when using as an ES module if you need the legacy behavior.\n */\nexport async function autoInit() {\n // Expose globals for PyScript interop\n window.pythonSystems = {};\n window.dynSimConfigs = {};\n\n // JS-side registration \u2014 called by the Python bridge wrapper.\n // The Python bridge (injected by injectPythonBridge) redefines\n // window.registerPythonSystem to wrap step functions with to_py()\n // conversion, then calls this function.\n window._dynsimJsRegister = function (containerId, stepFunction, config) {\n console.log('[DynSim] Registering Python system:', containerId);\n registry.register(containerId, stepFunction, config);\n\n if (document.readyState === 'complete' && typeof Plotly !== 'undefined') {\n initializeContainer(containerId);\n }\n };\n\n // Fallback: if the Python bridge hasn't loaded, provide a direct JS registration\n if (!window.registerPythonSystem) {\n window.registerPythonSystem = window._dynsimJsRegister;\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', async () => {\n await setupPyScriptContainers();\n initializeAllContainers();\n });\n } else {\n await setupPyScriptContainers();\n initializeAllContainers();\n }\n}\n\n// --- Internal helpers for autoInit ---\n\nconst controllers = {};\n\nfunction initializeContainer(containerId) {\n const container = document.getElementById(containerId);\n if (!container || container.querySelector('.dynsim-container')) return;\n\n const config = registry.getConfig(containerId);\n if (!config) return;\n\n controllers[containerId] = new SimulationController({\n container,\n config,\n stepProvider: () => registry.getStep(containerId)\n });\n controllers[containerId].start();\n}\n\nfunction initializeAllContainers() {\n let attempts = 0;\n const MAX_ATTEMPTS = 20;\n\n function tryInit() {\n attempts++;\n if (typeof Plotly === 'undefined') {\n if (attempts < MAX_ATTEMPTS) setTimeout(tryInit, 50);\n return;\n }\n\n const ids = registry.getContainerIds();\n if (ids.length === 0 && attempts < MAX_ATTEMPTS) {\n setTimeout(tryInit, 50);\n return;\n }\n\n ids.forEach(initializeContainer);\n }\n\n tryInit();\n}\n\nasync function setupPyScriptContainers() {\n // Wait for data file\n let attempts = 0;\n while (!window.dynSimSystemsData && attempts < 20) {\n await new Promise(resolve => setTimeout(resolve, 500));\n attempts++;\n }\n if (!window.dynSimSystemsData) return;\n\n // Wait for PyScript bootstrap\n attempts = 0;\n while (!window.executeDynSimCode && attempts < 40) {\n await new Promise(resolve => setTimeout(resolve, 500));\n attempts++;\n }\n if (!window.executeDynSimCode) return;\n\n const containers = document.querySelectorAll('.dynsim-python-container');\n for (const container of containers) {\n const systemData = window.dynSimSystemsData[container.id];\n if (!systemData) continue;\n\n try {\n window.dynSimConfigs[container.id] = systemData.config;\n window.executeDynSimCode(systemData.pythonCode, container.id, systemData.config);\n } catch (e) {\n console.error('[DynSim] Error processing container:', container.id, e);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["Simulation", "config", "stepProvider", "inputValue", "paramValues", "stepFn", "params", "result", "dataPoint", "d", "windowSize", "originalEnd", "pointsPerWindow", "bufferPoints", "targetPoints", "cutoff", "firstKeep", "t", "SimulationView", "_SimulationView", "container", "params", "initialX", "height", "plotType", "plotConfig", "spikeThreshold", "callbacks", "paramsDiv", "html", "param", "e", "slider", "layout", "result", "plotArrays", "xRange", "spikeTimes", "shapes", "t", "isRunning", "btn", "SimulationController", "container", "config", "stepProvider", "Simulation", "SimulationView", "inputValue", "paramValues", "e", "plotArrays", "xRange", "spikeTimes", "registry_exports", "__export", "getConfig", "getContainerIds", "getStep", "has", "register", "replaceStep", "systems", "toPlainObject", "value", "containerId", "stepFunction", "rawConfig", "cfg", "CodeEditor", "container", "containerId", "initialCode", "executePython", "e", "start", "end", "code", "config", "getConfig", "text", "color", "str", "PYTHON_BRIDGE", "injectPythonBridge", "script", "pyToJs", "value", "autoInit", "containerId", "stepFunction", "config", "register", "initializeContainer", "setupPyScriptContainers", "initializeAllContainers", "controllers", "container", "getConfig", "SimulationController", "getStep", "attempts", "MAX_ATTEMPTS", "tryInit", "ids", "getContainerIds", "resolve", "containers", "systemData", "e"]
7
+ }
@@ -0,0 +1,81 @@
1
+ var DynSim=(()=>{var f=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var S=(s,t)=>{for(var e in t)f(s,e,{get:t[e],enumerable:!0})},z=(s,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of D(t))!E.call(s,n)&&n!==e&&f(s,n,{get:()=>t[n],enumerable:!(i=k(t,n))||i.enumerable});return s};var J=s=>z(f({},"__esModule",{value:!0}),s);var L={};S(L,{CodeEditor:()=>m,Simulation:()=>l,SimulationController:()=>p,SimulationView:()=>a,autoInit:()=>u,registry:()=>d});var l=class{constructor(t,e){this.params=t.params,this.plotType=t.plotType||"timeseries",this.plotConfig=t.plotConfig||{},this.initialState=t.initialState||{t:0},this.initialX=t.initialX??0,this.dt=t.dt||.01,this.maxPoints=t.maxPoints||1e3,this.pauseTime=t.pauseTime??null,this.spikes=t.spikes||null,this.stepProvider=e,this.x=this.initialX,this.state={...this.initialState},this.time=0,this.plotData=[],this.spikeTimes=[]}step(t,e){let i=this.stepProvider();if(!i)throw new Error("No step function available");let n={...e,dt:this.dt},o=i(t,this.state,n);this.x=o[0],this.state=o[1],this.time+=this.dt,this.spikes&&this.state[this.spikes]&&this.spikeTimes.push(this.time);let r=this._collectDataPoint();return this.plotData.push(r),this._manageBuffer(),{x:this.x,state:this.state,dataPoint:r}}get paused(){return this.pauseTime!=null&&this.time>=this.pauseTime}reset(){this.x=this.initialX,this.state={...this.initialState},this.time=0,this.plotData=[],this.spikeTimes=[]}getPlotArrays(){return this.plotType==="3d"?{x:this.plotData.map(t=>t[0]),y:this.plotData.map(t=>t[1]),z:this.plotData.map(t=>t[2])}:{x:this.plotData.map(t=>t[0]),y:this.plotData.map(t=>t[1])}}getTimeseriesRange(){let t=this.plotConfig.xaxis?.range?.[1]-this.plotConfig.xaxis?.range?.[0]||50,e=this.plotConfig.xaxis?.range?.[1]||50;return this.time>e?[this.time-t,this.time]:this.plotConfig.xaxis?.range||[0,50]}_collectDataPoint(){return this.plotType==="3d"?[this.state.x||this.x,this.state.y||0,this.state.z||0]:this.plotType==="timeseries"?[this.time,this.x]:[this.x,this.state.y||0]}_manageBuffer(){if(this.plotType==="timeseries"){let t=this.plotConfig.xaxis?.range?.[1]||50,e=Math.ceil(t/this.dt),i=Math.ceil(e*.5),n=e+i;if(this.plotData.length>n*2&&(this.plotData=this.plotData.slice(-n),this.spikeTimes.length>0)){let o=this.plotData[0][0],r=this.spikeTimes.findIndex(y=>y>=o);r>0&&(this.spikeTimes=this.spikeTimes.slice(r))}}else this.plotData.length>this.maxPoints&&this.plotData.shift()}};var a=class s{constructor({container:t,params:e,initialX:i,height:n,plotType:o,plotConfig:r,spikeThreshold:y,callbacks:_}){this.container=t,this.params=e,this.initialX=i,this.height=n||400,this.plotType=o||"timeseries",this.plotConfig=r||{},this.spikeThreshold=y,this.callbacks=_||{},this.plotDiv=null,this.createHTML(),this.initPlot()}createHTML(){this.container.innerHTML=`
2
+ <div class="dynsim-container" style="font-family: Arial, sans-serif; font-size: 0.9em;">
3
+ <div class="dynsim-controls" style="background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 12px; box-sizing: border-box;">
4
+ <div class="dynsim-params"></div>
5
+ </div>
6
+ <div style="width: 100%; height: ${this.height}px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; overflow: hidden;">
7
+ <div class="dynsim-plot" style="width: 100%; height: 100%;"></div>
8
+ </div>
9
+ </div>
10
+ `,this._buildControls(),this.plotDiv=this.container.querySelector(".dynsim-plot"),typeof MathJax<"u"&&MathJax.typesetPromise&&MathJax.typesetPromise([this.container.querySelector(".dynsim-controls")])}_buildControls(){let t=this.container.querySelector(".dynsim-params"),e=`
11
+ <div style="display: flex; gap: 12px; align-items: center; margin-bottom: 8px;">
12
+ <label style="font-weight: 600; font-size: 0.85em; color: #0056b3; white-space: nowrap;">Input (x):</label>
13
+ <input type="range" class="dynsim-input"
14
+ min="-2" max="2" step="0.1" value="${this.initialX}"
15
+ style="flex: 1; height: 6px; min-width: 100px;">
16
+ <span class="dynsim-input-value" style="background: #cfe2ff; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${this.initialX.toFixed(2)}</span>
17
+ <button class="dynsim-reset" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Reset">
18
+ <svg width="20" height="20" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
19
+ <path d="M21 12a9 9 0 1 1-9 -9c2.5 0 4.8 1 6.5 2.5l.5 .5"/>
20
+ <path d="M21 3v6h-6"/>
21
+ </svg>
22
+ </button>
23
+ </div>
24
+ `;e+='<div style="display: flex; gap: 12px; align-items: center;">',e+=this.params.map(i=>`
25
+ <label style="font-weight: 600; font-size: 0.85em; white-space: nowrap;">${i.label}:</label>
26
+ <input type="range" class="dynsim-param" data-param="${i.id}"
27
+ min="${i.min}" max="${i.max}" step="${i.step}" value="${i.value}"
28
+ style="flex: 1; height: 6px; min-width: 100px;">
29
+ <span class="dynsim-param-value" style="background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;">${i.value.toFixed(2)}</span>
30
+ `).join(""),e+=`
31
+ <button class="dynsim-pause" style="background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;" title="Pause">
32
+ ${s.PAUSE_ICON}
33
+ </button>
34
+ </div>`,t.innerHTML=e,t.querySelector(".dynsim-input").addEventListener("input",i=>{i.target.closest("div").querySelector(".dynsim-input-value").textContent=parseFloat(i.target.value).toFixed(2)}),t.querySelectorAll(".dynsim-param").forEach(i=>{i.addEventListener("input",n=>{n.target.nextElementSibling.textContent=parseFloat(n.target.value).toFixed(2)})}),this.container.querySelector(".dynsim-reset").addEventListener("click",()=>this.callbacks.onReset?.()),this.container.querySelector(".dynsim-pause").addEventListener("click",()=>this.callbacks.onPauseToggle?.())}initPlot(){if(this.plotType==="3d")Plotly.newPlot(this.plotDiv,[{x:[],y:[],z:[],mode:"lines",type:"scatter3d",line:{color:"#2196f3",width:4}}],{title:this.plotConfig.title,scene:{xaxis:{title:this.plotConfig.xaxis?.title||"X"},yaxis:{title:this.plotConfig.yaxis?.title||"Y"},zaxis:{title:this.plotConfig.zaxis?.title||"Z"}},margin:{l:0,r:0,t:30,b:0}});else{let t={margin:{l:50,r:20,t:40,b:50}};this.plotConfig.title&&(t.title=this.plotConfig.title),this.plotConfig.xaxis&&(t.xaxis=this.plotConfig.xaxis),this.plotConfig.yaxis&&(t.yaxis=this.plotConfig.yaxis),Plotly.newPlot(this.plotDiv,[{x:[],y:[],mode:"lines",line:{color:"#2196f3",width:2}}],t)}}getInput(){return parseFloat(this.container.querySelector(".dynsim-input").value)}getParameters(){let t={};return this.container.querySelectorAll(".dynsim-param").forEach(e=>{t[e.dataset.param]=parseFloat(e.value)}),t}updatePlot(t,e,i){if(this.plotType==="3d")Plotly.animate(this.plotDiv,{data:[{x:t.x,y:t.y,z:t.z}]},{transition:{duration:0},frame:{duration:0}});else if(this.plotType==="timeseries"){let n={title:this.plotConfig.title,xaxis:{title:this.plotConfig.xaxis?.title||"Time",range:e},yaxis:this.plotConfig.yaxis,margin:{l:50,r:20,t:40,b:50}},o=[];if(this.spikeThreshold!=null&&o.push({type:"line",x0:0,x1:1,xref:"paper",y0:this.spikeThreshold,y1:this.spikeThreshold,line:{color:"grey",width:1,dash:"dash"}}),i&&i.length>0)for(let r of i)o.push({type:"line",x0:r,x1:r,y0:0,y1:1,yref:"paper",line:{color:"rgba(255, 0, 0, 0.4)",width:1}});o.length>0&&(n.shapes=o),Plotly.react(this.plotDiv,[{x:t.x,y:t.y,mode:"lines",line:{color:"#2196f3",width:2}}],n)}else Plotly.animate(this.plotDiv,{data:[{x:t.x,y:t.y}]},{transition:{duration:0},frame:{duration:0}})}setPauseState(t){let e=this.container.querySelector(".dynsim-pause");e.title=t?"Pause":"Play",e.innerHTML=t?s.PAUSE_ICON:s.PLAY_ICON}destroy(){this.container.innerHTML=""}};a.PAUSE_ICON='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="10" y1="15" x2="10" y2="9"></line><line x1="14" y1="15" x2="14" y2="9"></line></svg>';a.PLAY_ICON='<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polygon points="10 8 16 12 10 16 10 8"></polygon></svg>';var p=class{constructor({container:t,config:e,stepProvider:i}){this.isRunning=!0,this.animationId=null,this.simulation=new l(e,i),this.view=new a({container:t,params:e.params,initialX:e.initialX??0,height:e.height||400,plotType:e.plotType||"timeseries",plotConfig:e.plotConfig||{},spikeThreshold:e.spikeThreshold??null,callbacks:{onReset:()=>this.reset(),onPauseToggle:()=>this.togglePause()}})}start(){this.animationId&&cancelAnimationFrame(this.animationId),this.animate()}stop(){this.animationId&&(cancelAnimationFrame(this.animationId),this.animationId=null)}reset(){this.simulation.reset(),this.view.initPlot()}togglePause(){this.isRunning=!this.isRunning,this.isRunning&&this.simulation.paused&&(this.simulation.pauseTime=null),this.view.setPauseState(this.isRunning)}animate(){if(this.isRunning&&!this.simulation.paused){let n=this.view.getInput(),o=this.view.getParameters();try{this.simulation.step(n,o)}catch(r){console.error("[DynSim] Step error:",r),this.stop();return}this.simulation.paused&&(this.isRunning=!1,this.view.setPauseState(!1))}let t=this.simulation.getPlotArrays(),e=this.simulation.plotType==="timeseries"?this.simulation.getTimeseriesRange():void 0,i=this.simulation.spikes?this.simulation.spikeTimes:void 0;this.view.updatePlot(t,e,i),this.animationId=requestAnimationFrame(()=>this.animate())}destroy(){this.stop(),this.view.destroy()}};var d={};S(d,{getConfig:()=>c,getContainerIds:()=>w,getStep:()=>x,has:()=>I,register:()=>g,replaceStep:()=>q});var h={};function j(s){if(s==null)return s;if(typeof s.toJs=="function")try{return s.toJs({dict_converter:Object.fromEntries})}catch{try{return s.toJs()}catch{}}try{return JSON.parse(JSON.stringify(s))}catch{return s}}function g(s,t,e){let i=j(e);h[s]={step:t,config:{params:typeof i.params=="string"?JSON.parse(i.params):i.params||[],plotType:i.plotType||"timeseries",plotConfig:typeof i.plotConfig=="string"?JSON.parse(i.plotConfig):i.plotConfig||{},initialState:typeof i.initialState=="string"?JSON.parse(i.initialState):i.initialState||{t:0},initialX:i.initialX??0,height:i.height||400,dt:i.dt||.01,pauseTime:i.pauseTime??null,spikes:i.spikes||null,spikeThreshold:i.spikeThreshold??null}}}function x(s){return h[s]?.step||null}function c(s){return h[s]?.config||null}function q(s,t){h[s]&&(h[s].step=t)}function w(){return Object.keys(h)}function I(s){return s in h}var m=class{constructor({container:t,containerId:e,initialCode:i,executePython:n}){this.container=t,this.containerId=e,this.initialCode=i||"",this.executePython=n,this.textarea=null,this.statusEl=null,this.render()}render(){this.container.innerHTML=`
35
+ <div class="dynsim-editor" style="font-family: Arial, sans-serif; font-size: 0.9em; margin-bottom: 12px;">
36
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
37
+ <label style="font-weight: 600; font-size: 0.85em;">Python System Definition</label>
38
+ <div style="display: flex; gap: 8px; align-items: center;">
39
+ <span class="dynsim-editor-status" style="font-size: 0.8em; color: #666;"></span>
40
+ <button class="dynsim-editor-apply" style="
41
+ background: #0056b3; color: white; border: none; border-radius: 4px;
42
+ padding: 4px 12px; cursor: pointer; font-size: 0.85em;
43
+ ">Apply</button>
44
+ <button class="dynsim-editor-reset" style="
45
+ background: #6c757d; color: white; border: none; border-radius: 4px;
46
+ padding: 4px 12px; cursor: pointer; font-size: 0.85em;
47
+ ">Reset Code</button>
48
+ </div>
49
+ </div>
50
+ <textarea class="dynsim-editor-textarea" style="
51
+ width: 100%; min-height: 200px; font-family: monospace; font-size: 0.9em;
52
+ padding: 8px; border: 1px solid #ddd; border-radius: 6px;
53
+ box-sizing: border-box; resize: vertical; tab-size: 4;
54
+ " spellcheck="false">${this._escapeHtml(this.initialCode)}</textarea>
55
+ </div>
56
+ `,this.textarea=this.container.querySelector(".dynsim-editor-textarea"),this.statusEl=this.container.querySelector(".dynsim-editor-status"),this.container.querySelector(".dynsim-editor-apply").addEventListener("click",()=>this.apply()),this.container.querySelector(".dynsim-editor-reset").addEventListener("click",()=>this.resetCode()),this.textarea.addEventListener("keydown",t=>{if(t.key==="Tab"){t.preventDefault();let e=this.textarea.selectionStart,i=this.textarea.selectionEnd;this.textarea.value=this.textarea.value.substring(0,e)+" "+this.textarea.value.substring(i),this.textarea.selectionStart=this.textarea.selectionEnd=e+4}})}apply(){let t=this.textarea.value,e=c(this.containerId);try{this.executePython(t,this.containerId,e),this._setStatus("Applied","green")}catch(i){console.error("[DynSim Editor] Error applying code:",i),this._setStatus("Error: "+i.message,"red")}}resetCode(){this.textarea.value=this.initialCode,this._setStatus("Reset to original","#666")}getCode(){return this.textarea.value}_setStatus(t,e){this.statusEl.textContent=t,this.statusEl.style.color=e,setTimeout(()=>{this.statusEl.textContent===t&&(this.statusEl.textContent="")},3e3)}_escapeHtml(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}};var R=`
57
+ def _dynsim_init_bridge():
58
+ from pyscript import window as _w
59
+ from pyodide.ffi import create_proxy, to_js
60
+ from js import Object
61
+
62
+ _js_register = _w._dynsimJsRegister
63
+
64
+ def _register(container_id, step_fn, config):
65
+ def wrapped_step(x, state, params):
66
+ # Convert JS inputs to Python dicts
67
+ s = state.to_py() if hasattr(state, 'to_py') else state
68
+ p = params.to_py() if hasattr(params, 'to_py') else params
69
+ result = step_fn(float(x), s, p)
70
+ # Convert Python outputs to plain JS objects
71
+ return to_js(result, dict_converter=Object.fromEntries)
72
+ # create_proxy prevents the wrapped function from being garbage collected
73
+ # after this call returns \u2014 it's called repeatedly from requestAnimationFrame
74
+ _js_register(container_id, create_proxy(wrapped_step), config)
75
+
76
+ _w.registerPythonSystem = _register
77
+
78
+ _dynsim_init_bridge()
79
+ del _dynsim_init_bridge
80
+ `;function b(){let s=document.createElement("script");s.type="py",s.textContent=R,document.head.insertBefore(s,document.head.firstChild)}async function u(){window.pythonSystems={},window.dynSimConfigs={},window._dynsimJsRegister=function(s,t,e){console.log("[DynSim] Registering Python system:",s),g(s,t,e),document.readyState==="complete"&&typeof Plotly<"u"&&T(s)},window.registerPythonSystem||(window.registerPythonSystem=window._dynsimJsRegister),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",async()=>{await P(),C()}):(await P(),C())}var v={};function T(s){let t=document.getElementById(s);if(!t||t.querySelector(".dynsim-container"))return;let e=c(s);e&&(v[s]=new p({container:t,config:e,stepProvider:()=>x(s)}),v[s].start())}function C(){let s=0,t=20;function e(){if(s++,typeof Plotly>"u"){s<t&&setTimeout(e,50);return}let i=w();if(i.length===0&&s<t){setTimeout(e,50);return}i.forEach(T)}e()}async function P(){let s=0;for(;!window.dynSimSystemsData&&s<20;)await new Promise(e=>setTimeout(e,500)),s++;if(!window.dynSimSystemsData)return;for(s=0;!window.executeDynSimCode&&s<40;)await new Promise(e=>setTimeout(e,500)),s++;if(!window.executeDynSimCode)return;let t=document.querySelectorAll(".dynsim-python-container");for(let e of t){let i=window.dynSimSystemsData[e.id];if(i)try{window.dynSimConfigs[e.id]=i.config,window.executeDynSimCode(i.pythonCode,e.id,i.config)}catch(n){console.error("[DynSim] Error processing container:",e.id,n)}}}b();u();return J(L);})();
81
+ //# sourceMappingURL=dynsim.umd.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/umd-entry.js", "../src/simulation.js", "../src/view.js", "../src/controller.js", "../src/registry.js", "../src/editor.js", "../src/pybridge.js", "../src/index.js"],
4
+ "sourcesContent": ["/**\n * UMD entry point \u2014 auto-initializes for <script> tag usage.\n * Exposes everything on the global DynSim namespace and calls autoInit().\n *\n * Injects the Python bridge FIRST (synchronously) so that PyScript\n * processes it before any user <script type=\"py\"> tags.\n */\nexport { Simulation } from './simulation.js';\nexport { SimulationView } from './view.js';\nexport { SimulationController } from './controller.js';\nexport { CodeEditor } from './editor.js';\nexport * as registry from './registry.js';\nexport { autoInit } from './index.js';\n\nimport { injectPythonBridge } from './pybridge.js';\nimport { autoInit } from './index.js';\n\n// 1. Inject Python bridge before PyScript runs\ninjectPythonBridge();\n\n// 2. Set up JS-side registration and auto-init\nautoInit();\n", "/**\n * Pure simulation engine \u2014 no DOM, no Plotly.\n *\n * Manages state, stepping, and plot data buffering.\n * The step function is resolved via a stepProvider on each tick,\n * enabling live code replacement.\n */\nexport class Simulation {\n /**\n * @param {object} config\n * @param {Array} config.params - Parameter definitions [{id, label, min, max, step, value}]\n * @param {string} config.plotType - 'timeseries' | '3d' | '2d'\n * @param {object} config.plotConfig - Plotly layout config (title, axes, etc.)\n * @param {object} config.initialState - Initial state object\n * @param {number} config.initialX - Initial input/output value\n * @param {number} config.dt - Time step\n * @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)\n * @param {number} [config.maxPoints=1000] - Max buffered points (non-timeseries)\n * @param {number} [config.pauseTime=null] - Pause simulation at this time (null = run forever)\n * @param {string} [config.spikes=null] - State variable name to check for spikes (e.g. 'z'). Falsy = no spikes.\n * @param {function} stepProvider - () => stepFunction. Called each tick to get the current step function.\n */\n constructor(config, stepProvider) {\n this.params = config.params;\n this.plotType = config.plotType || 'timeseries';\n this.plotConfig = config.plotConfig || {};\n this.initialState = config.initialState || { t: 0 };\n this.initialX = config.initialX ?? 0;\n this.dt = config.dt || 0.01;\n this.maxPoints = config.maxPoints || 1000;\n this.pauseTime = config.pauseTime ?? null;\n this.spikes = config.spikes || null;\n\n this.stepProvider = stepProvider;\n\n this.x = this.initialX;\n this.state = { ...this.initialState };\n this.time = 0;\n this.plotData = [];\n this.spikeTimes = [];\n }\n\n /**\n * Advance one time step.\n * @param {number} inputValue - Current input from the user (slider)\n * @param {object} paramValues - Parameter values keyed by param ID\n * @returns {{ x: number, state: object, dataPoint: Array }} result of the step\n * @throws if stepProvider returns null or step function throws\n */\n step(inputValue, paramValues) {\n const stepFn = this.stepProvider();\n if (!stepFn) {\n throw new Error('No step function available');\n }\n\n // Add dt to params\n const params = { ...paramValues, dt: this.dt };\n\n const result = stepFn(inputValue, this.state, params);\n this.x = result[0];\n this.state = result[1];\n this.time += this.dt;\n\n // Record spike if the configured state variable is truthy\n if (this.spikes && this.state[this.spikes]) {\n this.spikeTimes.push(this.time);\n }\n\n const dataPoint = this._collectDataPoint();\n this.plotData.push(dataPoint);\n this._manageBuffer();\n\n return { x: this.x, state: this.state, dataPoint };\n }\n\n /**\n * Whether the simulation has reached its pause time.\n * @returns {boolean}\n */\n get paused() {\n return this.pauseTime != null && this.time >= this.pauseTime;\n }\n\n /**\n * Reset simulation to initial conditions.\n */\n reset() {\n this.x = this.initialX;\n this.state = { ...this.initialState };\n this.time = 0;\n this.plotData = [];\n this.spikeTimes = [];\n }\n\n /**\n * Get current plot data arrays suitable for Plotly.\n * @returns {object} { x, y, z? } arrays\n */\n getPlotArrays() {\n if (this.plotType === '3d') {\n return {\n x: this.plotData.map(d => d[0]),\n y: this.plotData.map(d => d[1]),\n z: this.plotData.map(d => d[2])\n };\n }\n return {\n x: this.plotData.map(d => d[0]),\n y: this.plotData.map(d => d[1])\n };\n }\n\n /**\n * Compute the current x-axis range for timeseries plots.\n * @returns {[number, number]} [min, max]\n */\n getTimeseriesRange() {\n const windowSize = (this.plotConfig.xaxis?.range?.[1] - this.plotConfig.xaxis?.range?.[0]) || 50;\n const originalEnd = this.plotConfig.xaxis?.range?.[1] || 50;\n\n if (this.time > originalEnd) {\n return [this.time - windowSize, this.time];\n }\n return this.plotConfig.xaxis?.range || [0, 50];\n }\n\n // --- Private ---\n\n _collectDataPoint() {\n if (this.plotType === '3d') {\n return [this.state.x || this.x, this.state.y || 0, this.state.z || 0];\n } else if (this.plotType === 'timeseries') {\n return [this.time, this.x];\n } else {\n return [this.x, this.state.y || 0];\n }\n }\n\n _manageBuffer() {\n if (this.plotType === 'timeseries') {\n const windowSize = this.plotConfig.xaxis?.range?.[1] || 50;\n const pointsPerWindow = Math.ceil(windowSize / this.dt);\n const bufferPoints = Math.ceil(pointsPerWindow * 0.5);\n const targetPoints = pointsPerWindow + bufferPoints;\n\n if (this.plotData.length > targetPoints * 2) {\n this.plotData = this.plotData.slice(-targetPoints);\n // Trim old spike times outside the buffer\n if (this.spikeTimes.length > 0) {\n const cutoff = this.plotData[0][0]; // earliest time in buffer\n const firstKeep = this.spikeTimes.findIndex(t => t >= cutoff);\n if (firstKeep > 0) this.spikeTimes = this.spikeTimes.slice(firstKeep);\n }\n }\n } else {\n if (this.plotData.length > this.maxPoints) {\n this.plotData.shift();\n }\n }\n }\n}\n", "/**\n * SimulationView \u2014 DOM + Plotly rendering.\n *\n * Creates the UI (sliders, buttons, plot area), reads user input,\n * and updates the Plotly chart. Emits events via callbacks.\n * Contains no simulation logic.\n */\nexport class SimulationView {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element to render into\n * @param {Array} options.params - Parameter definitions [{id, label, min, max, step, value}]\n * @param {number} options.initialX - Initial input value\n * @param {number} options.height - Plot height in pixels\n * @param {string} options.plotType - 'timeseries' | '3d' | '2d'\n * @param {object} options.plotConfig - Plotly layout config\n * @param {object} [options.callbacks] - { onReset, onPauseToggle }\n */\n constructor({ container, params, initialX, height, plotType, plotConfig, spikeThreshold, callbacks }) {\n this.container = container;\n this.params = params;\n this.initialX = initialX;\n this.height = height || 400;\n this.plotType = plotType || 'timeseries';\n this.plotConfig = plotConfig || {};\n this.spikeThreshold = spikeThreshold;\n this.callbacks = callbacks || {};\n\n this.plotDiv = null;\n\n this.createHTML();\n this.initPlot();\n }\n\n createHTML() {\n this.container.innerHTML = `\n <div class=\"dynsim-container\" style=\"font-family: Arial, sans-serif; font-size: 0.9em;\">\n <div class=\"dynsim-controls\" style=\"background: #f8f9fa; padding: 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 12px; box-sizing: border-box;\">\n <div class=\"dynsim-params\"></div>\n </div>\n <div style=\"width: 100%; height: ${this.height}px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; overflow: hidden;\">\n <div class=\"dynsim-plot\" style=\"width: 100%; height: 100%;\"></div>\n </div>\n </div>\n `;\n\n this._buildControls();\n this.plotDiv = this.container.querySelector('.dynsim-plot');\n\n // Typeset LaTeX in labels if MathJax is available\n if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {\n MathJax.typesetPromise([this.container.querySelector('.dynsim-controls')]);\n }\n }\n\n _buildControls() {\n const paramsDiv = this.container.querySelector('.dynsim-params');\n\n // Input slider row\n let html = `\n <div style=\"display: flex; gap: 12px; align-items: center; margin-bottom: 8px;\">\n <label style=\"font-weight: 600; font-size: 0.85em; color: #0056b3; white-space: nowrap;\">Input (x):</label>\n <input type=\"range\" class=\"dynsim-input\"\n min=\"-2\" max=\"2\" step=\"0.1\" value=\"${this.initialX}\"\n style=\"flex: 1; height: 6px; min-width: 100px;\">\n <span class=\"dynsim-input-value\" style=\"background: #cfe2ff; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;\">${this.initialX.toFixed(2)}</span>\n <button class=\"dynsim-reset\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;\" title=\"Reset\">\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" fill=\"none\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M21 12a9 9 0 1 1-9 -9c2.5 0 4.8 1 6.5 2.5l.5 .5\"/>\n <path d=\"M21 3v6h-6\"/>\n </svg>\n </button>\n </div>\n `;\n\n // Parameter sliders row\n html += `<div style=\"display: flex; gap: 12px; align-items: center;\">`;\n html += this.params.map(param => `\n <label style=\"font-weight: 600; font-size: 0.85em; white-space: nowrap;\">${param.label}:</label>\n <input type=\"range\" class=\"dynsim-param\" data-param=\"${param.id}\"\n min=\"${param.min}\" max=\"${param.max}\" step=\"${param.step}\" value=\"${param.value}\"\n style=\"flex: 1; height: 6px; min-width: 100px;\">\n <span class=\"dynsim-param-value\" style=\"background: #e9ecef; padding: 2px 8px; border-radius: 3px; font-size: 0.85em; min-width: 40px; text-align: center; font-family: monospace;\">${param.value.toFixed(2)}</span>\n `).join('');\n html += `\n <button class=\"dynsim-pause\" style=\"background: transparent; border: none; cursor: pointer; padding: 4px; display: flex; align-items: center;\" title=\"Pause\">\n ${SimulationView.PAUSE_ICON}\n </button>\n </div>`;\n\n paramsDiv.innerHTML = html;\n\n // Wire up slider display updates\n paramsDiv.querySelector('.dynsim-input').addEventListener('input', (e) => {\n e.target.closest('div').querySelector('.dynsim-input-value')\n .textContent = parseFloat(e.target.value).toFixed(2);\n });\n paramsDiv.querySelectorAll('.dynsim-param').forEach(slider => {\n slider.addEventListener('input', (e) => {\n e.target.nextElementSibling.textContent = parseFloat(e.target.value).toFixed(2);\n });\n });\n\n // Wire up button callbacks\n this.container.querySelector('.dynsim-reset')\n .addEventListener('click', () => this.callbacks.onReset?.());\n this.container.querySelector('.dynsim-pause')\n .addEventListener('click', () => this.callbacks.onPauseToggle?.());\n }\n\n initPlot() {\n if (this.plotType === '3d') {\n Plotly.newPlot(this.plotDiv, [{\n x: [], y: [], z: [],\n mode: 'lines',\n type: 'scatter3d',\n line: { color: '#2196f3', width: 4 }\n }], {\n title: this.plotConfig.title,\n scene: {\n xaxis: { title: this.plotConfig.xaxis?.title || 'X' },\n yaxis: { title: this.plotConfig.yaxis?.title || 'Y' },\n zaxis: { title: this.plotConfig.zaxis?.title || 'Z' }\n },\n margin: { l: 0, r: 0, t: 30, b: 0 }\n });\n } else {\n const layout = { margin: { l: 50, r: 20, t: 40, b: 50 } };\n if (this.plotConfig.title) layout.title = this.plotConfig.title;\n if (this.plotConfig.xaxis) layout.xaxis = this.plotConfig.xaxis;\n if (this.plotConfig.yaxis) layout.yaxis = this.plotConfig.yaxis;\n\n Plotly.newPlot(this.plotDiv, [{\n x: [], y: [],\n mode: 'lines',\n line: { color: '#2196f3', width: 2 }\n }], layout);\n }\n }\n\n /**\n * Read the current input slider value.\n * @returns {number}\n */\n getInput() {\n return parseFloat(this.container.querySelector('.dynsim-input').value);\n }\n\n /**\n * Read current parameter slider values as a dict keyed by param ID.\n * @returns {object}\n */\n getParameters() {\n const result = {};\n this.container.querySelectorAll('.dynsim-param').forEach(slider => {\n result[slider.dataset.param] = parseFloat(slider.value);\n });\n return result;\n }\n\n /**\n * Update the Plotly chart with new data.\n * @param {object} plotArrays - { x, y, z? } from Simulation.getPlotArrays()\n * @param {[number, number]} [xRange] - x-axis range for timeseries\n * @param {number[]} [spikeTimes] - spike times to render as vertical lines\n */\n updatePlot(plotArrays, xRange, spikeTimes) {\n if (this.plotType === '3d') {\n Plotly.animate(this.plotDiv, {\n data: [{ x: plotArrays.x, y: plotArrays.y, z: plotArrays.z }]\n }, { transition: { duration: 0 }, frame: { duration: 0 } });\n } else if (this.plotType === 'timeseries') {\n const layout = {\n title: this.plotConfig.title,\n xaxis: { title: this.plotConfig.xaxis?.title || 'Time', range: xRange },\n yaxis: this.plotConfig.yaxis,\n margin: { l: 50, r: 20, t: 40, b: 50 }\n };\n\n // Render spike markers and threshold line\n const shapes = [];\n\n // Spike threshold: horizontal dashed line\n if (this.spikeThreshold != null) {\n shapes.push({\n type: 'line',\n x0: 0, x1: 1, xref: 'paper',\n y0: this.spikeThreshold, y1: this.spikeThreshold,\n line: { color: 'grey', width: 1, dash: 'dash' }\n });\n }\n\n // Spike times: vertical lines\n if (spikeTimes && spikeTimes.length > 0) {\n for (const t of spikeTimes) {\n shapes.push({\n type: 'line',\n x0: t, x1: t,\n y0: 0, y1: 1, yref: 'paper',\n line: { color: 'rgba(255, 0, 0, 0.4)', width: 1 }\n });\n }\n }\n\n if (shapes.length > 0) layout.shapes = shapes;\n\n Plotly.react(this.plotDiv,\n [{ x: plotArrays.x, y: plotArrays.y, mode: 'lines', line: { color: '#2196f3', width: 2 } }],\n layout\n );\n } else {\n Plotly.animate(this.plotDiv, {\n data: [{ x: plotArrays.x, y: plotArrays.y }]\n }, { transition: { duration: 0 }, frame: { duration: 0 } });\n }\n }\n\n /**\n * Update the pause button icon.\n * @param {boolean} isRunning\n */\n setPauseState(isRunning) {\n const btn = this.container.querySelector('.dynsim-pause');\n btn.title = isRunning ? 'Pause' : 'Play';\n btn.innerHTML = isRunning ? SimulationView.PAUSE_ICON : SimulationView.PLAY_ICON;\n }\n\n destroy() {\n this.container.innerHTML = '';\n }\n}\n\nSimulationView.PAUSE_ICON = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><line x1=\"10\" y1=\"15\" x2=\"10\" y2=\"9\"></line><line x1=\"14\" y1=\"15\" x2=\"14\" y2=\"9\"></line></svg>`;\n\nSimulationView.PLAY_ICON = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><polygon points=\"10 8 16 12 10 16 10 8\"></polygon></svg>`;\n", "/**\n * SimulationController \u2014 wires Simulation + SimulationView.\n *\n * Owns the requestAnimationFrame loop. Each tick:\n * reads input from view \u2192 calls simulation.step() \u2192 updates view.\n */\nimport { Simulation } from './simulation.js';\nimport { SimulationView } from './view.js';\n\nexport class SimulationController {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element for the simulation view\n * @param {object} options.config - Parsed system config (from registry)\n * @param {function} options.stepProvider - () => stepFunction\n */\n constructor({ container, config, stepProvider }) {\n this.isRunning = true;\n this.animationId = null;\n\n this.simulation = new Simulation(config, stepProvider);\n\n this.view = new SimulationView({\n container,\n params: config.params,\n initialX: config.initialX ?? 0,\n height: config.height || 400,\n plotType: config.plotType || 'timeseries',\n plotConfig: config.plotConfig || {},\n spikeThreshold: config.spikeThreshold ?? null,\n callbacks: {\n onReset: () => this.reset(),\n onPauseToggle: () => this.togglePause()\n }\n });\n }\n\n start() {\n if (this.animationId) {\n cancelAnimationFrame(this.animationId);\n }\n this.animate();\n }\n\n stop() {\n if (this.animationId) {\n cancelAnimationFrame(this.animationId);\n this.animationId = null;\n }\n }\n\n reset() {\n this.simulation.reset();\n this.view.initPlot();\n }\n\n togglePause() {\n this.isRunning = !this.isRunning;\n // Clear pause time so the simulation can continue past it\n if (this.isRunning && this.simulation.paused) {\n this.simulation.pauseTime = null;\n }\n this.view.setPauseState(this.isRunning);\n }\n\n animate() {\n if (this.isRunning && !this.simulation.paused) {\n const inputValue = this.view.getInput();\n const paramValues = this.view.getParameters();\n\n try {\n this.simulation.step(inputValue, paramValues);\n } catch (e) {\n console.error('[DynSim] Step error:', e);\n this.stop();\n return;\n }\n\n // Auto-pause when pause time is reached\n if (this.simulation.paused) {\n this.isRunning = false;\n this.view.setPauseState(false);\n }\n }\n\n const plotArrays = this.simulation.getPlotArrays();\n const xRange = this.simulation.plotType === 'timeseries'\n ? this.simulation.getTimeseriesRange()\n : undefined;\n const spikeTimes = this.simulation.spikes ? this.simulation.spikeTimes : undefined;\n this.view.updatePlot(plotArrays, xRange, spikeTimes);\n\n this.animationId = requestAnimationFrame(() => this.animate());\n }\n\n destroy() {\n this.stop();\n this.view.destroy();\n }\n}\n", "/**\n * Registry for step functions, keyed by container ID.\n *\n * Supports live replacement: update a step function at any time\n * and the next simulation tick will pick it up via the stepProvider pattern.\n */\n\nconst systems = {};\n\n/**\n * Ensure a value is a plain JS object (not a PyProxy or other foreign wrapper).\n * Uses Pyodide's toJs() if available, otherwise falls back to JSON round-trip.\n */\nfunction toPlainObject(value) {\n if (value == null) return value;\n // Pyodide PyProxy \u2014 use toJs with dict_converter for proper dict\u2192object conversion\n if (typeof value.toJs === 'function') {\n try {\n return value.toJs({ dict_converter: Object.fromEntries });\n } catch {\n try { return value.toJs(); } catch {}\n }\n }\n // Fallback: JSON round-trip (works for plain JS objects)\n try {\n return JSON.parse(JSON.stringify(value));\n } catch {\n return value;\n }\n}\n\n/**\n * Register (or replace) a step function and config for a container.\n * @param {string} containerId\n * @param {function} stepFunction - step(x, state, params) => [x_new, state_new]\n * @param {object} rawConfig - Config object (plain JS or Python dict proxy \u2014 converted implicitly)\n */\nexport function register(containerId, stepFunction, rawConfig) {\n // Convert potential PyProxy to plain JS object\n const cfg = toPlainObject(rawConfig);\n systems[containerId] = {\n step: stepFunction,\n config: {\n params: typeof cfg.params === 'string' ? JSON.parse(cfg.params) : (cfg.params || []),\n plotType: cfg.plotType || 'timeseries',\n plotConfig: typeof cfg.plotConfig === 'string' ? JSON.parse(cfg.plotConfig) : (cfg.plotConfig || {}),\n initialState: typeof cfg.initialState === 'string' ? JSON.parse(cfg.initialState) : (cfg.initialState || { t: 0 }),\n initialX: cfg.initialX ?? 0,\n height: cfg.height || 400,\n dt: cfg.dt || 0.01,\n pauseTime: cfg.pauseTime ?? null,\n spikes: cfg.spikes || null,\n spikeThreshold: cfg.spikeThreshold ?? null\n }\n };\n}\n\n/**\n * Get the current step function for a container.\n * @param {string} containerId\n * @returns {function|null}\n */\nexport function getStep(containerId) {\n return systems[containerId]?.step || null;\n}\n\n/**\n * Get the parsed config for a container.\n * @param {string} containerId\n * @returns {object|null}\n */\nexport function getConfig(containerId) {\n return systems[containerId]?.config || null;\n}\n\n/**\n * Replace just the step function (for live code editing).\n * @param {string} containerId\n * @param {function} stepFunction\n */\nexport function replaceStep(containerId, stepFunction) {\n if (systems[containerId]) {\n systems[containerId].step = stepFunction;\n }\n}\n\n/**\n * Get all registered container IDs.\n * @returns {string[]}\n */\nexport function getContainerIds() {\n return Object.keys(systems);\n}\n\n/**\n * Check if a container is registered.\n * @param {string} containerId\n * @returns {boolean}\n */\nexport function has(containerId) {\n return containerId in systems;\n}\n", "/**\n * CodeEditor \u2014 in-page Python code editor with live replacement.\n *\n * Provides a textarea for editing the Python step function definition.\n * On \"Apply\", re-executes the code via PyScript/Pyodide and updates\n * the step function in the registry. The simulation continues\n * seamlessly with the new dynamics on the next tick.\n */\nimport * as registry from './registry.js';\n\nexport class CodeEditor {\n /**\n * @param {object} options\n * @param {HTMLElement} options.container - DOM element to render the editor into\n * @param {string} options.containerId - The simulation container ID (registry key)\n * @param {string} options.initialCode - Initial Python source code\n * @param {function} options.executePython - (code, containerId, config) => void\n * Function that executes Python code in the PyScript/Pyodide runtime.\n * The code should call registerPythonSystem which updates the registry.\n */\n constructor({ container, containerId, initialCode, executePython }) {\n this.container = container;\n this.containerId = containerId;\n this.initialCode = initialCode || '';\n this.executePython = executePython;\n this.textarea = null;\n this.statusEl = null;\n\n this.render();\n }\n\n render() {\n this.container.innerHTML = `\n <div class=\"dynsim-editor\" style=\"font-family: Arial, sans-serif; font-size: 0.9em; margin-bottom: 12px;\">\n <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;\">\n <label style=\"font-weight: 600; font-size: 0.85em;\">Python System Definition</label>\n <div style=\"display: flex; gap: 8px; align-items: center;\">\n <span class=\"dynsim-editor-status\" style=\"font-size: 0.8em; color: #666;\"></span>\n <button class=\"dynsim-editor-apply\" style=\"\n background: #0056b3; color: white; border: none; border-radius: 4px;\n padding: 4px 12px; cursor: pointer; font-size: 0.85em;\n \">Apply</button>\n <button class=\"dynsim-editor-reset\" style=\"\n background: #6c757d; color: white; border: none; border-radius: 4px;\n padding: 4px 12px; cursor: pointer; font-size: 0.85em;\n \">Reset Code</button>\n </div>\n </div>\n <textarea class=\"dynsim-editor-textarea\" style=\"\n width: 100%; min-height: 200px; font-family: monospace; font-size: 0.9em;\n padding: 8px; border: 1px solid #ddd; border-radius: 6px;\n box-sizing: border-box; resize: vertical; tab-size: 4;\n \" spellcheck=\"false\">${this._escapeHtml(this.initialCode)}</textarea>\n </div>\n `;\n\n this.textarea = this.container.querySelector('.dynsim-editor-textarea');\n this.statusEl = this.container.querySelector('.dynsim-editor-status');\n\n this.container.querySelector('.dynsim-editor-apply')\n .addEventListener('click', () => this.apply());\n\n this.container.querySelector('.dynsim-editor-reset')\n .addEventListener('click', () => this.resetCode());\n\n // Tab key inserts spaces instead of changing focus\n this.textarea.addEventListener('keydown', (e) => {\n if (e.key === 'Tab') {\n e.preventDefault();\n const start = this.textarea.selectionStart;\n const end = this.textarea.selectionEnd;\n this.textarea.value =\n this.textarea.value.substring(0, start) +\n ' ' +\n this.textarea.value.substring(end);\n this.textarea.selectionStart = this.textarea.selectionEnd = start + 4;\n }\n });\n }\n\n /**\n * Re-execute the current code and update the registry.\n */\n apply() {\n const code = this.textarea.value;\n const config = registry.getConfig(this.containerId);\n\n try {\n this.executePython(code, this.containerId, config);\n this._setStatus('Applied', 'green');\n } catch (e) {\n console.error('[DynSim Editor] Error applying code:', e);\n this._setStatus('Error: ' + e.message, 'red');\n }\n }\n\n /**\n * Reset the textarea to the initial code.\n */\n resetCode() {\n this.textarea.value = this.initialCode;\n this._setStatus('Reset to original', '#666');\n }\n\n /**\n * Get the current code from the editor.\n * @returns {string}\n */\n getCode() {\n return this.textarea.value;\n }\n\n _setStatus(text, color) {\n this.statusEl.textContent = text;\n this.statusEl.style.color = color;\n // Clear status after 3 seconds\n setTimeout(() => {\n if (this.statusEl.textContent === text) {\n this.statusEl.textContent = '';\n }\n }, 3000);\n }\n\n _escapeHtml(str) {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n}\n", "/**\n * PyBridge \u2014 transparent type conversion between JS and Python (Pyodide).\n *\n * Injects a Python-side bridge script that wraps step functions so that\n * JS objects are automatically converted to Python dicts before the user's\n * step function is called. Neither side needs to know about the other.\n *\n * Flow:\n * 1. UMD load \u2192 injectPythonBridge() inserts <script type=\"py\"> into DOM\n * 2. PyScript processes it before user scripts (document order)\n * 3. Python bridge redefines window.registerPythonSystem to wrap step functions\n * 4. User's Python code calls registerPythonSystem as normal\n * 5. The wrapper converts JsProxy args to Python dicts, calls real step,\n * and returns the result\n */\n\nconst PYTHON_BRIDGE = `\ndef _dynsim_init_bridge():\n from pyscript import window as _w\n from pyodide.ffi import create_proxy, to_js\n from js import Object\n\n _js_register = _w._dynsimJsRegister\n\n def _register(container_id, step_fn, config):\n def wrapped_step(x, state, params):\n # Convert JS inputs to Python dicts\n s = state.to_py() if hasattr(state, 'to_py') else state\n p = params.to_py() if hasattr(params, 'to_py') else params\n result = step_fn(float(x), s, p)\n # Convert Python outputs to plain JS objects\n return to_js(result, dict_converter=Object.fromEntries)\n # create_proxy prevents the wrapped function from being garbage collected\n # after this call returns \u2014 it's called repeatedly from requestAnimationFrame\n _js_register(container_id, create_proxy(wrapped_step), config)\n\n _w.registerPythonSystem = _register\n\n_dynsim_init_bridge()\ndel _dynsim_init_bridge\n`;\n\n/**\n * Inject the Python bridge script into the DOM.\n * Must be called synchronously during UMD script load (before PyScript processes scripts).\n */\nexport function injectPythonBridge() {\n const script = document.createElement('script');\n script.type = 'py';\n script.textContent = PYTHON_BRIDGE;\n // Insert as first child of <head> so it runs before user's <script type=\"py\"> tags\n document.head.insertBefore(script, document.head.firstChild);\n}\n\n/**\n * Convert a Python proxy value to a plain JS value.\n */\nexport function pyToJs(value) {\n if (value != null && typeof value.toJs === 'function') {\n try {\n return value.toJs({ dict_converter: Object.fromEntries });\n } catch {\n try { return value.toJs(); } catch {}\n }\n }\n return value;\n}\n", "/**\n * dynsim \u2014 Interactive dynamical systems simulator.\n *\n * ES module entry point. Exports all public API.\n */\nexport { Simulation } from './simulation.js';\nexport { SimulationView } from './view.js';\nexport { SimulationController } from './controller.js';\nexport { CodeEditor } from './editor.js';\nexport * as registry from './registry.js';\nexport { pyToJs, injectPythonBridge } from './pybridge.js';\n\nimport { SimulationController } from './controller.js';\nimport * as registry from './registry.js';\n\n/**\n * Auto-initialize: expose globals for PyScript interop and\n * bootstrap PyScript containers. Called automatically by the UMD build.\n * Call manually when using as an ES module if you need the legacy behavior.\n */\nexport async function autoInit() {\n // Expose globals for PyScript interop\n window.pythonSystems = {};\n window.dynSimConfigs = {};\n\n // JS-side registration \u2014 called by the Python bridge wrapper.\n // The Python bridge (injected by injectPythonBridge) redefines\n // window.registerPythonSystem to wrap step functions with to_py()\n // conversion, then calls this function.\n window._dynsimJsRegister = function (containerId, stepFunction, config) {\n console.log('[DynSim] Registering Python system:', containerId);\n registry.register(containerId, stepFunction, config);\n\n if (document.readyState === 'complete' && typeof Plotly !== 'undefined') {\n initializeContainer(containerId);\n }\n };\n\n // Fallback: if the Python bridge hasn't loaded, provide a direct JS registration\n if (!window.registerPythonSystem) {\n window.registerPythonSystem = window._dynsimJsRegister;\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', async () => {\n await setupPyScriptContainers();\n initializeAllContainers();\n });\n } else {\n await setupPyScriptContainers();\n initializeAllContainers();\n }\n}\n\n// --- Internal helpers for autoInit ---\n\nconst controllers = {};\n\nfunction initializeContainer(containerId) {\n const container = document.getElementById(containerId);\n if (!container || container.querySelector('.dynsim-container')) return;\n\n const config = registry.getConfig(containerId);\n if (!config) return;\n\n controllers[containerId] = new SimulationController({\n container,\n config,\n stepProvider: () => registry.getStep(containerId)\n });\n controllers[containerId].start();\n}\n\nfunction initializeAllContainers() {\n let attempts = 0;\n const MAX_ATTEMPTS = 20;\n\n function tryInit() {\n attempts++;\n if (typeof Plotly === 'undefined') {\n if (attempts < MAX_ATTEMPTS) setTimeout(tryInit, 50);\n return;\n }\n\n const ids = registry.getContainerIds();\n if (ids.length === 0 && attempts < MAX_ATTEMPTS) {\n setTimeout(tryInit, 50);\n return;\n }\n\n ids.forEach(initializeContainer);\n }\n\n tryInit();\n}\n\nasync function setupPyScriptContainers() {\n // Wait for data file\n let attempts = 0;\n while (!window.dynSimSystemsData && attempts < 20) {\n await new Promise(resolve => setTimeout(resolve, 500));\n attempts++;\n }\n if (!window.dynSimSystemsData) return;\n\n // Wait for PyScript bootstrap\n attempts = 0;\n while (!window.executeDynSimCode && attempts < 40) {\n await new Promise(resolve => setTimeout(resolve, 500));\n attempts++;\n }\n if (!window.executeDynSimCode) return;\n\n const containers = document.querySelectorAll('.dynsim-python-container');\n for (const container of containers) {\n const systemData = window.dynSimSystemsData[container.id];\n if (!systemData) continue;\n\n try {\n window.dynSimConfigs[container.id] = systemData.config;\n window.executeDynSimCode(systemData.pythonCode, container.id, systemData.config);\n } catch (e) {\n console.error('[DynSim] Error processing container:', container.id, e);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["umd_entry_exports", "__export", "CodeEditor", "Simulation", "SimulationController", "SimulationView", "autoInit", "registry_exports", "Simulation", "config", "stepProvider", "inputValue", "paramValues", "stepFn", "params", "result", "dataPoint", "d", "windowSize", "originalEnd", "pointsPerWindow", "bufferPoints", "targetPoints", "cutoff", "firstKeep", "t", "SimulationView", "_SimulationView", "container", "params", "initialX", "height", "plotType", "plotConfig", "spikeThreshold", "callbacks", "paramsDiv", "html", "param", "e", "slider", "layout", "result", "plotArrays", "xRange", "spikeTimes", "shapes", "t", "isRunning", "btn", "SimulationController", "container", "config", "stepProvider", "Simulation", "SimulationView", "inputValue", "paramValues", "e", "plotArrays", "xRange", "spikeTimes", "registry_exports", "__export", "getConfig", "getContainerIds", "getStep", "has", "register", "replaceStep", "systems", "toPlainObject", "value", "containerId", "stepFunction", "rawConfig", "cfg", "CodeEditor", "container", "containerId", "initialCode", "executePython", "e", "start", "end", "code", "config", "getConfig", "text", "color", "str", "PYTHON_BRIDGE", "injectPythonBridge", "script", "autoInit", "containerId", "stepFunction", "config", "register", "initializeContainer", "setupPyScriptContainers", "initializeAllContainers", "controllers", "container", "getConfig", "SimulationController", "getStep", "attempts", "MAX_ATTEMPTS", "tryInit", "ids", "getContainerIds", "resolve", "containers", "systemData", "e", "injectPythonBridge", "autoInit"]
7
+ }