demathpy 0.0.2__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- demathpy/__init__.py +5 -15
- demathpy/ode.py +358 -112
- demathpy/pde.py +756 -329
- demathpy/symbols.py +20 -2
- demathpy-0.1.1.dist-info/METADATA +207 -0
- demathpy-0.1.1.dist-info/RECORD +8 -0
- {demathpy-0.0.2.dist-info → demathpy-0.1.1.dist-info}/WHEEL +1 -1
- demathpy-0.0.2.dist-info/METADATA +0 -106
- demathpy-0.0.2.dist-info/RECORD +0 -8
- {demathpy-0.0.2.dist-info → demathpy-0.1.1.dist-info}/licenses/LICENSE +0 -0
demathpy/__init__.py
CHANGED
|
@@ -2,32 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from .symbols import normalize_symbols, normalize_lhs
|
|
4
4
|
from .pde import (
|
|
5
|
+
PDE,
|
|
5
6
|
normalize_pde,
|
|
6
7
|
init_grid,
|
|
7
|
-
sample_gradient,
|
|
8
8
|
parse_pde,
|
|
9
|
-
evaluate_rhs,
|
|
10
|
-
evaluate_scalar,
|
|
11
9
|
step_pdes,
|
|
12
|
-
evaluate_rhs_compiled,
|
|
13
|
-
evaluate_scalar_compiled,
|
|
14
|
-
step_compiled_pdes,
|
|
15
10
|
)
|
|
16
|
-
from .ode import
|
|
11
|
+
from .ode import ODE, parse_ode
|
|
17
12
|
|
|
18
13
|
__all__ = [
|
|
14
|
+
"PDE",
|
|
15
|
+
"ODE",
|
|
19
16
|
"normalize_symbols",
|
|
20
17
|
"normalize_lhs",
|
|
21
18
|
"normalize_pde",
|
|
22
19
|
"init_grid",
|
|
23
|
-
"sample_gradient",
|
|
24
20
|
"parse_pde",
|
|
25
|
-
"evaluate_rhs",
|
|
26
|
-
"evaluate_scalar",
|
|
27
21
|
"step_pdes",
|
|
28
|
-
"
|
|
29
|
-
"evaluate_scalar_compiled",
|
|
30
|
-
"step_compiled_pdes",
|
|
31
|
-
"robust_parse",
|
|
32
|
-
"parse_odes_to_function",
|
|
22
|
+
"parse_ode",
|
|
33
23
|
]
|
demathpy/ode.py
CHANGED
|
@@ -1,131 +1,377 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Ordinary Differential Equation (ODE) utilities.
|
|
3
|
+
|
|
4
|
+
Provides a class-based ODE solver similar to the PDE module,
|
|
5
|
+
but for systems dependent only on time (t).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict, List, Tuple, Any, Optional
|
|
11
|
+
|
|
2
12
|
import re
|
|
3
|
-
import
|
|
13
|
+
import json
|
|
4
14
|
import numpy as np
|
|
5
|
-
from sympy.parsing.sympy_parser import parse_expr, standard_transformations, implicit_multiplication_application, convert_xor
|
|
6
15
|
|
|
16
|
+
from .symbols import normalize_symbols, normalize_lhs
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"""
|
|
13
|
-
if "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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):
|
|
18
|
+
|
|
19
|
+
def _preprocess_expr(expr: str) -> str:
|
|
20
|
+
expr = (expr or "").strip()
|
|
21
|
+
expr = re.sub(r"\(\s*approx\s*\)", "", expr, flags=re.IGNORECASE)
|
|
22
|
+
expr = re.sub(r"\bapprox\b", "", expr, flags=re.IGNORECASE)
|
|
23
|
+
if "=" in expr:
|
|
24
|
+
expr = expr.split("=")[-1]
|
|
25
|
+
expr = normalize_symbols(expr)
|
|
26
|
+
return expr.strip()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_ode(ode: str) -> str:
|
|
30
|
+
return (ode or "").strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parse_ode(ode: str) -> Tuple[str, int, str, str]:
|
|
32
34
|
"""
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- Caret for power (x^2 -> x**2)
|
|
36
|
-
- Aliases 'y' to 'z' for 2D convenience
|
|
35
|
+
Returns (var, order, lhs_coeff_expr, rhs_expr).
|
|
36
|
+
Parses "dy/dt = -y" or "d^2y/dt^2 = -y".
|
|
37
37
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
ode = normalize_ode(ode)
|
|
39
|
+
if "=" not in ode:
|
|
40
|
+
# Assume implicit "dy/dt =" ?? No, safer to default to u, 1st order
|
|
41
|
+
return "u", 1, "1", ode
|
|
42
|
+
|
|
43
|
+
lhs, rhs = ode.split("=", 1)
|
|
44
|
+
lhs = normalize_lhs(lhs.strip())
|
|
45
|
+
rhs = rhs.strip()
|
|
46
|
+
|
|
47
|
+
def _extract_coeff(lhs_expr: str, deriv_expr: str) -> str:
|
|
48
|
+
coeff = lhs_expr.replace(deriv_expr, "").strip()
|
|
49
|
+
if not coeff:
|
|
50
|
+
return "1"
|
|
51
|
+
coeff = coeff.strip("*")
|
|
52
|
+
return _preprocess_expr(coeff) or "1"
|
|
53
|
+
|
|
54
|
+
# Updated regex for ODE derivatives: dy/dt, d^2x/dt^2
|
|
55
|
+
pattern = r"(?:∂|d)\s*(?:\^?(\d+))?\s*([a-zA-Z_]\w*)\s*/\s*(?:∂|d)t\s*(?:\^?(\d+))?"
|
|
56
|
+
m = re.search(pattern, lhs)
|
|
57
|
+
|
|
58
|
+
if m:
|
|
59
|
+
ord1 = m.group(1)
|
|
60
|
+
var = m.group(2)
|
|
61
|
+
ord2 = m.group(3)
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
order = 1
|
|
64
|
+
if ord1:
|
|
65
|
+
order = int(ord1)
|
|
66
|
+
elif ord2:
|
|
67
|
+
order = int(ord2)
|
|
68
|
+
|
|
69
|
+
coeff = _extract_coeff(lhs, m.group(0))
|
|
70
|
+
return var, order, coeff, _preprocess_expr(rhs)
|
|
71
|
+
|
|
72
|
+
# Fallback if no dy/dt found on lhs?
|
|
73
|
+
# Maybe user wrote "y' = ..." (not supported by regex yet)
|
|
74
|
+
return "u", 1, "1", _preprocess_expr(rhs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ODE:
|
|
78
|
+
equation: str
|
|
79
|
+
desc: str
|
|
80
|
+
|
|
81
|
+
u: np.ndarray
|
|
82
|
+
u_shape: List[str] # ["x"] or ["x", "y"] for vector systems
|
|
42
83
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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)
|
|
84
|
+
initial: List[str] # ["x=1", "y=0"]
|
|
85
|
+
|
|
86
|
+
external_variables: Dict[str, float]
|
|
87
|
+
time: float
|
|
88
|
+
|
|
89
|
+
_u_t: np.ndarray | None = None # For 2nd order or momentum
|
|
90
|
+
|
|
91
|
+
def __init__(self, equation: str = "", desc: str = "", u_shape: List[str] = None):
|
|
92
|
+
self.equation = equation
|
|
93
|
+
self.desc = desc
|
|
94
|
+
self.u = np.array([])
|
|
95
|
+
|
|
96
|
+
if u_shape:
|
|
97
|
+
self.u_shape = u_shape
|
|
98
|
+
elif equation:
|
|
99
|
+
# Infer from equation
|
|
100
|
+
var, _, _, _ = parse_ode(equation)
|
|
101
|
+
if var and var != "u":
|
|
102
|
+
self.u_shape = [var]
|
|
103
|
+
else:
|
|
104
|
+
self.u_shape = ["u"]
|
|
78
105
|
else:
|
|
79
|
-
|
|
80
|
-
except json.JSONDecodeError as e:
|
|
81
|
-
print(f"Failed to decode JSON from LLM: {e}")
|
|
82
|
-
return None
|
|
106
|
+
self.u_shape = ["u"]
|
|
83
107
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
self.initial = []
|
|
109
|
+
self.external_variables = {}
|
|
110
|
+
self.time = 0.0
|
|
111
|
+
self._u_t = None
|
|
112
|
+
|
|
113
|
+
def init_state(self, shape: tuple = (1,)):
|
|
114
|
+
"""
|
|
115
|
+
Initialize the state array y (self.u).
|
|
116
|
+
For a scalar ODE ("du/dt = ..."), shape corresponds to batch size (N independent systems).
|
|
117
|
+
For a vector ODE (u_shape=["x", "v"]), self.u will be (2, N) if shape=(N,).
|
|
118
|
+
"""
|
|
119
|
+
self.time = 0.0
|
|
120
|
+
|
|
121
|
+
if not self.u_shape:
|
|
122
|
+
self.u_shape = ["u"]
|
|
123
|
+
|
|
124
|
+
num_components = len(self.u_shape)
|
|
125
|
+
|
|
126
|
+
# If user passed a single int, wrap it
|
|
127
|
+
if isinstance(shape, int):
|
|
128
|
+
shape = (shape,)
|
|
129
|
+
|
|
130
|
+
if num_components > 1:
|
|
131
|
+
self.u = np.zeros((num_components, *shape), dtype=float)
|
|
132
|
+
else:
|
|
133
|
+
self.u = np.zeros(shape, dtype=float)
|
|
134
|
+
|
|
135
|
+
self._u_t = np.zeros_like(self.u)
|
|
136
|
+
|
|
137
|
+
def set_initial_state(self):
|
|
138
|
+
"""
|
|
139
|
+
Parses `self.initial` and applies to `self.u`.
|
|
140
|
+
"""
|
|
141
|
+
env = self._build_eval_env()
|
|
142
|
+
env["t"] = 0.0
|
|
143
|
+
|
|
144
|
+
if not self.initial:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
for ic_eqn in self.initial:
|
|
148
|
+
if "=" not in ic_eqn: continue
|
|
149
|
+
lhs, rhs = ic_eqn.split("=", 1)
|
|
150
|
+
lhs = lhs.strip()
|
|
151
|
+
rhs_expr = rhs.strip()
|
|
152
|
+
|
|
153
|
+
# Determine target component
|
|
154
|
+
target_idx = None
|
|
155
|
+
target_name = "u"
|
|
156
|
+
|
|
157
|
+
m = re.match(r"^([a-zA-Z_]\w*)", lhs)
|
|
158
|
+
if m:
|
|
159
|
+
target_name = m.group(1)
|
|
160
|
+
|
|
161
|
+
if target_name == "u" and len(self.u_shape) == 1:
|
|
162
|
+
target_idx = None
|
|
163
|
+
elif target_name in self.u_shape:
|
|
164
|
+
target_idx = self.u_shape.index(target_name)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
rhs_expr = _preprocess_expr(rhs_expr)
|
|
168
|
+
val = eval(rhs_expr, {}, env)
|
|
169
|
+
|
|
170
|
+
if target_idx is None:
|
|
171
|
+
if np.shape(val) == np.shape(self.u):
|
|
172
|
+
self.u[:] = val
|
|
173
|
+
else:
|
|
174
|
+
self.u[:] = val # Broadcast
|
|
175
|
+
elif len(self.u_shape) == 1 and target_idx == 0:
|
|
176
|
+
self.u[:] = val
|
|
177
|
+
else:
|
|
178
|
+
if target_idx < len(self.u):
|
|
179
|
+
self.u[target_idx][:] = val
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"Failed to set IC '{ic_eqn}': {e}")
|
|
182
|
+
|
|
183
|
+
def _build_eval_env(self) -> Dict[str, object]:
|
|
184
|
+
|
|
185
|
+
def pos(u): return np.maximum(u, 0.0)
|
|
186
|
+
def sign(u): return np.sign(u)
|
|
187
|
+
def step_fn(u): return np.heaviside(u, 1.0)
|
|
188
|
+
|
|
189
|
+
env = {
|
|
190
|
+
"np": np,
|
|
191
|
+
"sin": np.sin, "cos": np.cos, "tan": np.tan,
|
|
192
|
+
"sinh": np.sinh, "cosh": np.cosh, "tanh": np.tanh,
|
|
193
|
+
"arcsin": np.arcsin, "arccos": np.arccos, "arctan": np.arctan,
|
|
194
|
+
"log": np.log, "log10": np.log10, "log2": np.log2,
|
|
195
|
+
"exp": np.exp, "sqrt": np.sqrt, "abs": np.abs,
|
|
196
|
+
"pi": np.pi, "inf": np.inf,
|
|
197
|
+
"pos": pos, "sign": sign, "step": step_fn
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
env.update(self.external_variables)
|
|
201
|
+
|
|
202
|
+
if "t" not in env:
|
|
203
|
+
env["t"] = self.time
|
|
204
|
+
|
|
205
|
+
return env
|
|
206
|
+
|
|
207
|
+
def evaluate_rhs(self, rhs_expr: str, env: Dict[str, Any]) -> np.ndarray:
|
|
208
|
+
rhs_expr = _preprocess_expr(rhs_expr)
|
|
209
|
+
return eval(rhs_expr, {}, env)
|
|
89
210
|
|
|
90
|
-
|
|
91
|
-
|
|
211
|
+
def evaluate_scalar(self, expr: str, env: Dict[str, Any]) -> float:
|
|
212
|
+
if expr in ("", "1"): return 1.0
|
|
213
|
+
val = self.evaluate_rhs(expr, env)
|
|
214
|
+
if np.isscalar(val): return float(val)
|
|
215
|
+
return float(np.mean(val))
|
|
216
|
+
|
|
217
|
+
def step(self, dt: float):
|
|
218
|
+
var_name, order, coeff_expr, rhs_expr = parse_ode(self.equation)
|
|
219
|
+
|
|
220
|
+
self.time += dt
|
|
221
|
+
env = self._build_eval_env()
|
|
222
|
+
env["t"] = self.time
|
|
223
|
+
|
|
224
|
+
# Inject state
|
|
225
|
+
if len(self.u_shape) == 1:
|
|
226
|
+
name = self.u_shape[0]
|
|
227
|
+
env[name] = self.u
|
|
228
|
+
v_t = self._u_t if self._u_t is not None else np.zeros_like(self.u)
|
|
229
|
+
env[f"{name}_t"] = v_t
|
|
230
|
+
else:
|
|
231
|
+
for i, name in enumerate(self.u_shape):
|
|
232
|
+
env[name] = self.u[i]
|
|
233
|
+
v_t = self._u_t[i] if self._u_t is not None else np.zeros_like(self.u[i])
|
|
234
|
+
env[f"{name}_t"] = v_t
|
|
235
|
+
# Also inject 'u' as general access if needed, or maybe not to avoid confusion?
|
|
236
|
+
# PDE does env["u"] = self.u.
|
|
237
|
+
env["u"] = self.u
|
|
238
|
+
env["u_t"] = self._u_t if self._u_t is not None else np.zeros_like(self.u)
|
|
239
|
+
|
|
92
240
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
241
|
+
rhs = self.evaluate_rhs(rhs_expr, env)
|
|
242
|
+
if isinstance(rhs, list):
|
|
243
|
+
rhs = np.array(rhs)
|
|
244
|
+
|
|
245
|
+
coeff = self.evaluate_scalar(coeff_expr, env)
|
|
246
|
+
forcing = rhs / (coeff if coeff else 1.0)
|
|
247
|
+
|
|
248
|
+
# Ensure shape compatibility if forcing is lower dim (e.g. constant vector [1,2] vs shape (2, N))
|
|
249
|
+
if isinstance(forcing, np.ndarray) and forcing.ndim < self.u.ndim:
|
|
250
|
+
diff = self.u.ndim - forcing.ndim
|
|
251
|
+
# Assume alignment on first dimension (components)
|
|
252
|
+
# Expand trailing dims
|
|
253
|
+
new_shape = forcing.shape + (1,) * diff
|
|
254
|
+
forcing = forcing.reshape(new_shape)
|
|
255
|
+
|
|
256
|
+
target_indices = []
|
|
257
|
+
if var_name == "u" and len(self.u_shape) == 1:
|
|
258
|
+
target_indices = [None]
|
|
259
|
+
elif var_name in self.u_shape:
|
|
260
|
+
idx = self.u_shape.index(var_name)
|
|
261
|
+
target_indices = [idx]
|
|
262
|
+
elif var_name == "u" and len(self.u_shape) > 1:
|
|
263
|
+
target_indices = range(len(self.u_shape))
|
|
95
264
|
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
265
|
+
# 2nd Order Euler / Semi-Implicit
|
|
266
|
+
if order == 2:
|
|
267
|
+
if self._u_t is None: self._u_t = np.zeros_like(self.u)
|
|
268
|
+
|
|
269
|
+
if var_name == "u" and len(self.u_shape) > 1:
|
|
270
|
+
self._u_t += dt * forcing
|
|
271
|
+
self.u += dt * self._u_t
|
|
272
|
+
elif len(target_indices) == 1:
|
|
273
|
+
idx = target_indices[0]
|
|
274
|
+
# Handle flat u for single component
|
|
275
|
+
is_flat = (len(self.u_shape) == 1)
|
|
276
|
+
|
|
277
|
+
target_u = self.u if (idx is None or is_flat) else self.u[idx]
|
|
278
|
+
target_ut = self._u_t if (idx is None or is_flat) else self._u_t[idx]
|
|
279
|
+
|
|
280
|
+
target_ut[:] += dt * forcing
|
|
281
|
+
target_u[:] += dt * target_ut
|
|
282
|
+
|
|
283
|
+
else:
|
|
284
|
+
# 1st Order Euler
|
|
285
|
+
if var_name == "u" and len(self.u_shape) > 1:
|
|
286
|
+
self.u += dt * forcing
|
|
287
|
+
elif len(target_indices) == 1:
|
|
288
|
+
idx = target_indices[0]
|
|
289
|
+
if idx is None or (len(self.u_shape) == 1):
|
|
290
|
+
self.u[:] += dt * forcing
|
|
291
|
+
else:
|
|
292
|
+
self.u[idx] += dt * forcing
|
|
293
|
+
|
|
100
294
|
except Exception as e:
|
|
101
|
-
print(f"Error
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
295
|
+
print(f"Error stepping ODE: {e}")
|
|
296
|
+
raise e
|
|
297
|
+
|
|
298
|
+
def get_grid(self, u_state: np.ndarray = None, dt: float = 0.0) -> np.ndarray:
|
|
299
|
+
"""
|
|
300
|
+
Calculates the change (du) or rate of change (forcing/dydt) for the current state
|
|
301
|
+
without modifying the internal state.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
u_state: Optional state to substitute self.u
|
|
305
|
+
dt: if > 0, returns delta. If 0, returns rate.
|
|
306
|
+
"""
|
|
307
|
+
original_u = self.u
|
|
308
|
+
original_u_t = self._u_t
|
|
309
|
+
|
|
310
|
+
if u_state is not None:
|
|
311
|
+
self.u = u_state
|
|
312
|
+
if self._u_t is not None and self._u_t.shape != u_state.shape:
|
|
313
|
+
self._u_t = np.zeros_like(u_state)
|
|
314
|
+
elif self._u_t is None:
|
|
315
|
+
self._u_t = np.zeros_like(u_state)
|
|
316
|
+
|
|
317
|
+
var_name, order, coeff_expr, rhs_expr = parse_ode(self.equation)
|
|
109
318
|
|
|
319
|
+
env = self._build_eval_env()
|
|
320
|
+
env["t"] = self.time
|
|
321
|
+
|
|
322
|
+
if len(self.u_shape) == 1:
|
|
323
|
+
name = self.u_shape[0]
|
|
324
|
+
env[name] = self.u
|
|
325
|
+
v_t = self._u_t if self._u_t is not None else np.zeros_like(self.u)
|
|
326
|
+
env[f"{name}_t"] = v_t
|
|
327
|
+
else:
|
|
328
|
+
for i, name in enumerate(self.u_shape):
|
|
329
|
+
env[name] = self.u[i]
|
|
330
|
+
v_t = self._u_t[i] if self._u_t is not None else np.zeros_like(self.u[i])
|
|
331
|
+
env[f"{name}_t"] = v_t
|
|
332
|
+
env["u"] = self.u
|
|
333
|
+
env["u_t"] = self._u_t if self._u_t is not None else np.zeros_like(self.u)
|
|
334
|
+
|
|
110
335
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
336
|
+
rhs = self.evaluate_rhs(rhs_expr, env)
|
|
337
|
+
if isinstance(rhs, list):
|
|
338
|
+
rhs = np.array(rhs)
|
|
126
339
|
|
|
340
|
+
coeff = self.evaluate_scalar(coeff_expr, env)
|
|
341
|
+
forcing = rhs / (coeff if coeff else 1.0)
|
|
342
|
+
|
|
343
|
+
if order == 2:
|
|
344
|
+
# 2nd Order
|
|
345
|
+
if dt > 0:
|
|
346
|
+
v = self._u_t if self._u_t is not None else np.zeros_like(self.u)
|
|
347
|
+
du = dt * v + 0.5 * (dt**2) * forcing
|
|
348
|
+
else:
|
|
349
|
+
du = forcing
|
|
350
|
+
else:
|
|
351
|
+
# 1st Order
|
|
352
|
+
if dt > 0:
|
|
353
|
+
du = forcing * dt
|
|
354
|
+
else:
|
|
355
|
+
du = forcing
|
|
356
|
+
|
|
127
357
|
except Exception as e:
|
|
128
|
-
|
|
129
|
-
|
|
358
|
+
self.u = original_u
|
|
359
|
+
self._u_t = original_u_t
|
|
360
|
+
raise e
|
|
361
|
+
|
|
362
|
+
self.u = original_u
|
|
363
|
+
self._u_t = original_u_t
|
|
364
|
+
return du
|
|
365
|
+
|
|
366
|
+
def to_json(self) -> str:
|
|
367
|
+
return json.dumps({
|
|
368
|
+
"equation": self.equation,
|
|
369
|
+
"desc": self.desc,
|
|
370
|
+
"u_shape": self.u_shape,
|
|
371
|
+
"initial": self.initial,
|
|
372
|
+
"variables": self.external_variables,
|
|
373
|
+
"time": self.time
|
|
374
|
+
}, indent=2)
|
|
130
375
|
|
|
131
|
-
|
|
376
|
+
def __str__(self):
|
|
377
|
+
return self.to_json()
|