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 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.
@@ -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
@@ -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