demathpy 0.1.0__tar.gz
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.
- demathpy-0.1.0/LICENSE +21 -0
- demathpy-0.1.0/PKG-INFO +160 -0
- demathpy-0.1.0/README.md +146 -0
- demathpy-0.1.0/pyproject.toml +29 -0
- demathpy-0.1.0/src/demathpy/__init__.py +23 -0
- demathpy-0.1.0/src/demathpy/ode.py +131 -0
- demathpy-0.1.0/src/demathpy/pde.py +849 -0
- demathpy-0.1.0/src/demathpy/symbols.py +412 -0
demathpy-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Misekai
|
|
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.
|
demathpy-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: demathpy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PDE/ODE math backend
|
|
5
|
+
Author: Misekai
|
|
6
|
+
Author-email: Misekai <mcore-us@misekai.net>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy>=1.24.0
|
|
10
|
+
Requires-Dist: sympy>=1.12.0
|
|
11
|
+
Requires-Dist: typing>=3.10.0.0
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Demathpy
|
|
16
|
+
|
|
17
|
+
A Python library for parsing and safely evaluating symbolic Ordinary and Partial Differential Equations (ODEs/PDEs) on numerical grids.
|
|
18
|
+
|
|
19
|
+
This repository provides:
|
|
20
|
+
- A class-based `PDE` solver (`from demathpy import PDE`) for running simulations.
|
|
21
|
+
- A lightweight **symbol normalizer** that converts human-readable mathematical notation into valid Python expressions.
|
|
22
|
+
- Equation-based Boundary Conditions and Initial Conditions.
|
|
23
|
+
- Built-in support for common differential operators and vector calculus notation using Finite Differences.
|
|
24
|
+
|
|
25
|
+
### Key Features
|
|
26
|
+
|
|
27
|
+
#### 1. The PDE Class
|
|
28
|
+
|
|
29
|
+
The core of the library is the `PDE` class. It manages the grid state, parsing, and time-stepping.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from demathpy import PDE
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
# Define a Heat Equation: du/dt = Laplacian(u)
|
|
36
|
+
p = PDE("du/dt = lap(u)", space_axis=["x", "y"])
|
|
37
|
+
|
|
38
|
+
# Configure the grid
|
|
39
|
+
p.init_grid(width=100, height=100, dx=0.5)
|
|
40
|
+
|
|
41
|
+
# Set Initial Conditions using equations
|
|
42
|
+
p.initial = ["u = exp(-(x-25)**2 - (y-25)**2)"]
|
|
43
|
+
p.set_initial_state()
|
|
44
|
+
|
|
45
|
+
# Set Boundary Conditions using equations
|
|
46
|
+
# Format: "axis(coord) = value" or "axis=coord = value"
|
|
47
|
+
p.boundry = [
|
|
48
|
+
"x=0 = 1.0", # Left boundary (x=0) is fixed at 1.0
|
|
49
|
+
"x=100 = 0.0", # Right boundary (x=100) is fixed at 0.0
|
|
50
|
+
"y=0 = 0.0", # Bottom boundary is 0.0
|
|
51
|
+
"periodic" # Other unset boundaries (y=100) default to periodic or 0
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Run simulation
|
|
55
|
+
for _ in range(100):
|
|
56
|
+
p.step(dt=0.01)
|
|
57
|
+
|
|
58
|
+
print(p.u.mean())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
#### 2. Equation-Based Configuration
|
|
62
|
+
|
|
63
|
+
You can configure boundaries and initial states using string equations instead of manual array manipulation.
|
|
64
|
+
|
|
65
|
+
**Boundary Conditions (`p.boundry` list):**
|
|
66
|
+
- **Dirichlet:** `x=0 = 1.0` (Fixes value at boundary)
|
|
67
|
+
- **Neumann:** `dx(u) = 0` (Not fully exposed yet, currently defaults to Dirichlet logic if value provided).
|
|
68
|
+
- **Periodic:** Use `periodic` keyword or leave empty for default periodic behavior (if implemented).
|
|
69
|
+
|
|
70
|
+
**Initial Conditions (`p.initial` list):**
|
|
71
|
+
- **Scalar:** `u = sin(x) * cos(y)`
|
|
72
|
+
- **Vector Components:** `ux = 1.0`, `uy = 0.0` (if `p.u_shape = ["ux", "uy"]`)
|
|
73
|
+
|
|
74
|
+
#### 3. Vector Fields
|
|
75
|
+
|
|
76
|
+
The solver supports vector-valued PDEs (e.g., Navier-Stokes, Reaction-Diffusion systems).
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# 2D Advection: du/dt = - (u · ∇) u
|
|
80
|
+
p = PDE("du/dt = -advect(u, u)", space_axis=["x", "y"])
|
|
81
|
+
p.u_shape = ["ux", "uy"] # Define component names
|
|
82
|
+
p.init_grid(width=50, height=50, dx=1.0)
|
|
83
|
+
|
|
84
|
+
# Initialize Vortex
|
|
85
|
+
p.initial = [
|
|
86
|
+
"ux = -sin(y)",
|
|
87
|
+
"uy = sin(x)"
|
|
88
|
+
]
|
|
89
|
+
p.set_initial_state()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Supported Operators
|
|
93
|
+
|
|
94
|
+
The parser recognizes and maps these to NumPy finite difference functions:
|
|
95
|
+
|
|
96
|
+
- **Derivatives:** `du/dt`, `dx(u)`, `dy(u)`, `dz(u)`
|
|
97
|
+
- **Second Derivatives:** `dxx(u)`, `dzz(u)`
|
|
98
|
+
- **Laplacian:** `lap(u)` or `∇²u`
|
|
99
|
+
- **Gradient:** `grad(u)` or `∇u` (Returns vector)
|
|
100
|
+
- **Divergence:** `div(u)` or `∇·u` (Expects vector input)
|
|
101
|
+
- **Advection:** `advect(velocity, field)` -> `(velocity · ∇) field`
|
|
102
|
+
- **Math Functions:** `sin, cos, exp, log, abs, sqrt, tanh` ...
|
|
103
|
+
|
|
104
|
+
### Symbol Normalization
|
|
105
|
+
|
|
106
|
+
The parser supports Unicode and mathematical shorthand:
|
|
107
|
+
- `α, β, γ` → `alpha, beta, gamma`
|
|
108
|
+
- `u²` → `u**2`
|
|
109
|
+
- `|u|` → `abs(u)`
|
|
110
|
+
|
|
111
|
+
### Workflow & Visualization
|
|
112
|
+
|
|
113
|
+
To integrate `Demathpy` into visualization software or interactive notebooks, you can use the `get_grid()` method to probe the field dynamics without advancing the simulation time.
|
|
114
|
+
|
|
115
|
+
#### Visualization Step-by-Step
|
|
116
|
+
|
|
117
|
+
1. **Initialize**:
|
|
118
|
+
```python
|
|
119
|
+
p = PDE("du/dt = lap(u) - u**3 + u", space_axis=["x", "y"])
|
|
120
|
+
p.init_grid(width=20, height=20, dx=0.5)
|
|
121
|
+
p.initial = ["u = 0.1 * sin(x)"]
|
|
122
|
+
p.boundry = ["periodic"]
|
|
123
|
+
p.set_initial_state()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. **Probe the Vector Field (du/dt)**:
|
|
127
|
+
Use `get_grid(dt=0)` to get the instantaneous rate of change. This is useful for visualizing flow fields or phase plots.
|
|
128
|
+
```python
|
|
129
|
+
# Get Rate of Change (RHS of PDE)
|
|
130
|
+
du_dt = p.get_grid(dt=0)
|
|
131
|
+
|
|
132
|
+
# Or calculate the hypothetical next step delta
|
|
133
|
+
delta_u = p.get_grid(dt=0.01)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
3. **Predict on Arbitrary States**:
|
|
137
|
+
You can evaluate the PDE on a hypothetical state `u_test` without updating the solver's internal state. This is useful for drawing vector fields in phase space.
|
|
138
|
+
```python
|
|
139
|
+
# Create a test state
|
|
140
|
+
test_u = np.sin(p.u)
|
|
141
|
+
|
|
142
|
+
# Calculate how the PDE would evolve this state
|
|
143
|
+
# Returns the rate of change for the test state
|
|
144
|
+
response = p.get_grid(u_state=test_u, dt=0)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
4. **Run Simulation Loops**:
|
|
148
|
+
```python
|
|
149
|
+
import matplotlib.pyplot as plt
|
|
150
|
+
|
|
151
|
+
for i in range(100):
|
|
152
|
+
p.step(dt=0.01)
|
|
153
|
+
if i % 10 == 0:
|
|
154
|
+
plt.imshow(p.u) # Visualization logic
|
|
155
|
+
# plt.show()
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### License
|
|
159
|
+
|
|
160
|
+
MIT
|
demathpy-0.1.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Demathpy
|
|
2
|
+
|
|
3
|
+
A Python library for parsing and safely evaluating symbolic Ordinary and Partial Differential Equations (ODEs/PDEs) on numerical grids.
|
|
4
|
+
|
|
5
|
+
This repository provides:
|
|
6
|
+
- A class-based `PDE` solver (`from demathpy import PDE`) for running simulations.
|
|
7
|
+
- A lightweight **symbol normalizer** that converts human-readable mathematical notation into valid Python expressions.
|
|
8
|
+
- Equation-based Boundary Conditions and Initial Conditions.
|
|
9
|
+
- Built-in support for common differential operators and vector calculus notation using Finite Differences.
|
|
10
|
+
|
|
11
|
+
### Key Features
|
|
12
|
+
|
|
13
|
+
#### 1. The PDE Class
|
|
14
|
+
|
|
15
|
+
The core of the library is the `PDE` class. It manages the grid state, parsing, and time-stepping.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from demathpy import PDE
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
# Define a Heat Equation: du/dt = Laplacian(u)
|
|
22
|
+
p = PDE("du/dt = lap(u)", space_axis=["x", "y"])
|
|
23
|
+
|
|
24
|
+
# Configure the grid
|
|
25
|
+
p.init_grid(width=100, height=100, dx=0.5)
|
|
26
|
+
|
|
27
|
+
# Set Initial Conditions using equations
|
|
28
|
+
p.initial = ["u = exp(-(x-25)**2 - (y-25)**2)"]
|
|
29
|
+
p.set_initial_state()
|
|
30
|
+
|
|
31
|
+
# Set Boundary Conditions using equations
|
|
32
|
+
# Format: "axis(coord) = value" or "axis=coord = value"
|
|
33
|
+
p.boundry = [
|
|
34
|
+
"x=0 = 1.0", # Left boundary (x=0) is fixed at 1.0
|
|
35
|
+
"x=100 = 0.0", # Right boundary (x=100) is fixed at 0.0
|
|
36
|
+
"y=0 = 0.0", # Bottom boundary is 0.0
|
|
37
|
+
"periodic" # Other unset boundaries (y=100) default to periodic or 0
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Run simulation
|
|
41
|
+
for _ in range(100):
|
|
42
|
+
p.step(dt=0.01)
|
|
43
|
+
|
|
44
|
+
print(p.u.mean())
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### 2. Equation-Based Configuration
|
|
48
|
+
|
|
49
|
+
You can configure boundaries and initial states using string equations instead of manual array manipulation.
|
|
50
|
+
|
|
51
|
+
**Boundary Conditions (`p.boundry` list):**
|
|
52
|
+
- **Dirichlet:** `x=0 = 1.0` (Fixes value at boundary)
|
|
53
|
+
- **Neumann:** `dx(u) = 0` (Not fully exposed yet, currently defaults to Dirichlet logic if value provided).
|
|
54
|
+
- **Periodic:** Use `periodic` keyword or leave empty for default periodic behavior (if implemented).
|
|
55
|
+
|
|
56
|
+
**Initial Conditions (`p.initial` list):**
|
|
57
|
+
- **Scalar:** `u = sin(x) * cos(y)`
|
|
58
|
+
- **Vector Components:** `ux = 1.0`, `uy = 0.0` (if `p.u_shape = ["ux", "uy"]`)
|
|
59
|
+
|
|
60
|
+
#### 3. Vector Fields
|
|
61
|
+
|
|
62
|
+
The solver supports vector-valued PDEs (e.g., Navier-Stokes, Reaction-Diffusion systems).
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# 2D Advection: du/dt = - (u · ∇) u
|
|
66
|
+
p = PDE("du/dt = -advect(u, u)", space_axis=["x", "y"])
|
|
67
|
+
p.u_shape = ["ux", "uy"] # Define component names
|
|
68
|
+
p.init_grid(width=50, height=50, dx=1.0)
|
|
69
|
+
|
|
70
|
+
# Initialize Vortex
|
|
71
|
+
p.initial = [
|
|
72
|
+
"ux = -sin(y)",
|
|
73
|
+
"uy = sin(x)"
|
|
74
|
+
]
|
|
75
|
+
p.set_initial_state()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Supported Operators
|
|
79
|
+
|
|
80
|
+
The parser recognizes and maps these to NumPy finite difference functions:
|
|
81
|
+
|
|
82
|
+
- **Derivatives:** `du/dt`, `dx(u)`, `dy(u)`, `dz(u)`
|
|
83
|
+
- **Second Derivatives:** `dxx(u)`, `dzz(u)`
|
|
84
|
+
- **Laplacian:** `lap(u)` or `∇²u`
|
|
85
|
+
- **Gradient:** `grad(u)` or `∇u` (Returns vector)
|
|
86
|
+
- **Divergence:** `div(u)` or `∇·u` (Expects vector input)
|
|
87
|
+
- **Advection:** `advect(velocity, field)` -> `(velocity · ∇) field`
|
|
88
|
+
- **Math Functions:** `sin, cos, exp, log, abs, sqrt, tanh` ...
|
|
89
|
+
|
|
90
|
+
### Symbol Normalization
|
|
91
|
+
|
|
92
|
+
The parser supports Unicode and mathematical shorthand:
|
|
93
|
+
- `α, β, γ` → `alpha, beta, gamma`
|
|
94
|
+
- `u²` → `u**2`
|
|
95
|
+
- `|u|` → `abs(u)`
|
|
96
|
+
|
|
97
|
+
### Workflow & Visualization
|
|
98
|
+
|
|
99
|
+
To integrate `Demathpy` into visualization software or interactive notebooks, you can use the `get_grid()` method to probe the field dynamics without advancing the simulation time.
|
|
100
|
+
|
|
101
|
+
#### Visualization Step-by-Step
|
|
102
|
+
|
|
103
|
+
1. **Initialize**:
|
|
104
|
+
```python
|
|
105
|
+
p = PDE("du/dt = lap(u) - u**3 + u", space_axis=["x", "y"])
|
|
106
|
+
p.init_grid(width=20, height=20, dx=0.5)
|
|
107
|
+
p.initial = ["u = 0.1 * sin(x)"]
|
|
108
|
+
p.boundry = ["periodic"]
|
|
109
|
+
p.set_initial_state()
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
2. **Probe the Vector Field (du/dt)**:
|
|
113
|
+
Use `get_grid(dt=0)` to get the instantaneous rate of change. This is useful for visualizing flow fields or phase plots.
|
|
114
|
+
```python
|
|
115
|
+
# Get Rate of Change (RHS of PDE)
|
|
116
|
+
du_dt = p.get_grid(dt=0)
|
|
117
|
+
|
|
118
|
+
# Or calculate the hypothetical next step delta
|
|
119
|
+
delta_u = p.get_grid(dt=0.01)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
3. **Predict on Arbitrary States**:
|
|
123
|
+
You can evaluate the PDE on a hypothetical state `u_test` without updating the solver's internal state. This is useful for drawing vector fields in phase space.
|
|
124
|
+
```python
|
|
125
|
+
# Create a test state
|
|
126
|
+
test_u = np.sin(p.u)
|
|
127
|
+
|
|
128
|
+
# Calculate how the PDE would evolve this state
|
|
129
|
+
# Returns the rate of change for the test state
|
|
130
|
+
response = p.get_grid(u_state=test_u, dt=0)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
4. **Run Simulation Loops**:
|
|
134
|
+
```python
|
|
135
|
+
import matplotlib.pyplot as plt
|
|
136
|
+
|
|
137
|
+
for i in range(100):
|
|
138
|
+
p.step(dt=0.01)
|
|
139
|
+
if i % 10 == 0:
|
|
140
|
+
plt.imshow(p.u) # Visualization logic
|
|
141
|
+
# plt.show()
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### License
|
|
145
|
+
|
|
146
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
|
|
2
|
+
[project]
|
|
3
|
+
name = "demathpy"
|
|
4
|
+
version = "0.1.0"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name="Misekai", email="mcore-us@misekai.net" },
|
|
7
|
+
]
|
|
8
|
+
description = "PDE/ODE math backend"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"numpy>=1.24.0",
|
|
13
|
+
"sympy>=1.12.0",
|
|
14
|
+
"typing>=3.10.0.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
license = "MIT"
|
|
19
|
+
license-files = ["LICEN[CS]E*"]
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build >= 0.9.26, <0.10.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
24
|
+
|
|
25
|
+
[dependency-groups]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=9.0.2",
|
|
28
|
+
]
|
|
29
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""demathpy: PDE/ODE math backend for rde-core."""
|
|
2
|
+
|
|
3
|
+
from .symbols import normalize_symbols, normalize_lhs
|
|
4
|
+
from .pde import (
|
|
5
|
+
PDE,
|
|
6
|
+
normalize_pde,
|
|
7
|
+
init_grid,
|
|
8
|
+
parse_pde,
|
|
9
|
+
step_pdes,
|
|
10
|
+
)
|
|
11
|
+
from .ode import robust_parse, parse_odes_to_function
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PDE",
|
|
15
|
+
"normalize_symbols",
|
|
16
|
+
"normalize_lhs",
|
|
17
|
+
"normalize_pde",
|
|
18
|
+
"init_grid",
|
|
19
|
+
"parse_pde",
|
|
20
|
+
"step_pdes",
|
|
21
|
+
"robust_parse",
|
|
22
|
+
"parse_odes_to_function",
|
|
23
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import sympy
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application, convert_xor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _convert_ternary(expr: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Convert a single C-style ternary (cond ? a : b) into SymPy Piecewise.
|
|
11
|
+
Supports one level (no nesting).
|
|
12
|
+
"""
|
|
13
|
+
if "?" not in expr or ":" not in expr:
|
|
14
|
+
return expr
|
|
15
|
+
|
|
16
|
+
# naive split for single ternary
|
|
17
|
+
# pattern: <cond> ? <a> : <b>
|
|
18
|
+
parts = expr.split("?")
|
|
19
|
+
if len(parts) != 2:
|
|
20
|
+
return expr
|
|
21
|
+
cond = parts[0].strip()
|
|
22
|
+
rest = parts[1]
|
|
23
|
+
if ":" not in rest:
|
|
24
|
+
return expr
|
|
25
|
+
a, b = rest.split(":", 1)
|
|
26
|
+
a = a.strip()
|
|
27
|
+
b = b.strip()
|
|
28
|
+
return f"Piecewise(({a}, {cond}), ({b}, True))"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def robust_parse(expr_str):
|
|
32
|
+
"""
|
|
33
|
+
Parses a string into a SymPy expression with relaxed syntax rules:
|
|
34
|
+
- Implicit multiplication (5x -> 5*x)
|
|
35
|
+
- Caret for power (x^2 -> x**2)
|
|
36
|
+
- Aliases 'y' to 'z' for 2D convenience
|
|
37
|
+
"""
|
|
38
|
+
if not isinstance(expr_str, str):
|
|
39
|
+
return sympy.sympify(expr_str)
|
|
40
|
+
|
|
41
|
+
transformations = (standard_transformations + (implicit_multiplication_application, convert_xor))
|
|
42
|
+
|
|
43
|
+
# Define symbols and alias y -> z
|
|
44
|
+
x, z, vx, vz, t, pid = sympy.symbols('x z vx vz t id')
|
|
45
|
+
local_dict = {
|
|
46
|
+
'x': x, 'z': z, 'y': z, 'vx': vx, 'vz': vz, 't': t, 'id': pid,
|
|
47
|
+
'pi': sympy.pi, 'e': sympy.E
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Ensure common functions are recognized (Abs not abs)
|
|
51
|
+
local_dict.update({
|
|
52
|
+
'sin': sympy.sin,
|
|
53
|
+
'cos': sympy.cos,
|
|
54
|
+
'tan': sympy.tan,
|
|
55
|
+
'exp': sympy.exp,
|
|
56
|
+
'sqrt': sympy.sqrt,
|
|
57
|
+
'log': sympy.log,
|
|
58
|
+
'abs': sympy.Abs,
|
|
59
|
+
'Abs': sympy.Abs,
|
|
60
|
+
'Piecewise': sympy.Piecewise,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
pre = _convert_ternary(expr_str)
|
|
65
|
+
return parse_expr(pre, transformations=transformations, local_dict=local_dict)
|
|
66
|
+
except Exception:
|
|
67
|
+
# Fallback
|
|
68
|
+
return sympy.sympify(expr_str, locals=local_dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_odes_to_function(ode_json_str):
|
|
72
|
+
"""
|
|
73
|
+
Parses a JSON string of ODEs and returns a dynamic update function.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
if isinstance(ode_json_str, str):
|
|
77
|
+
odes = json.loads(ode_json_str)
|
|
78
|
+
else:
|
|
79
|
+
odes = ode_json_str
|
|
80
|
+
except json.JSONDecodeError as e:
|
|
81
|
+
print(f"Failed to decode JSON from LLM: {e}")
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
# Define standard symbols
|
|
85
|
+
x, z, vx, vz, t = sympy.symbols('x z vx vz t')
|
|
86
|
+
|
|
87
|
+
deriv_map = {}
|
|
88
|
+
keys = ['dx', 'dz', 'dvx', 'dvz']
|
|
89
|
+
|
|
90
|
+
for key in keys:
|
|
91
|
+
expr_str = odes.get(key, "0")
|
|
92
|
+
try:
|
|
93
|
+
# Parse the expression safely using robust parser
|
|
94
|
+
expr = robust_parse(str(expr_str))
|
|
95
|
+
|
|
96
|
+
# Create a localized function
|
|
97
|
+
# Arguments match the order we will call them
|
|
98
|
+
func = sympy.lambdify((x, z, vx, vz, t), expr, modules=['numpy', 'math'])
|
|
99
|
+
deriv_map[key] = func
|
|
100
|
+
except Exception as e:
|
|
101
|
+
print(f"Error parsing expression for {key}: {e}")
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def dynamics(particle, dt):
|
|
105
|
+
# Current state
|
|
106
|
+
cx, cz, cvx, cvz = particle.x, particle.z, particle.vx, particle.vz
|
|
107
|
+
# We assume particle might track time, or we just pass 0 if autonomous
|
|
108
|
+
ct = getattr(particle, 'time', 0.0)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# Calculate derivatives
|
|
112
|
+
val_dx = deriv_map['dx'](cx, cz, cvx, cvz, ct)
|
|
113
|
+
val_dz = deriv_map['dz'](cx, cz, cvx, cvz, ct)
|
|
114
|
+
val_dvx = deriv_map['dvx'](cx, cz, cvx, cvz, ct)
|
|
115
|
+
val_dvz = deriv_map['dvz'](cx, cz, cvx, cvz, ct)
|
|
116
|
+
|
|
117
|
+
# Simple Euler Integration
|
|
118
|
+
particle.x += float(val_dx) * dt
|
|
119
|
+
particle.z += float(val_dz) * dt
|
|
120
|
+
particle.vx += float(val_dvx) * dt
|
|
121
|
+
particle.vz += float(val_dvz) * dt
|
|
122
|
+
|
|
123
|
+
# Update time if tracked
|
|
124
|
+
if hasattr(particle, 'time'):
|
|
125
|
+
particle.time += dt
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
# Prevent crashing the renderer on math errors (e.g. div by zero)
|
|
129
|
+
print(f"Runtime error in dynamics: {e}")
|
|
130
|
+
|
|
131
|
+
return dynamics
|