Skip to content

Commit 48d8aa2

Browse files
Fix merge
1 parent b9d7859 commit 48d8aa2

19 files changed

Lines changed: 497 additions & 55 deletions

epde/solver/base.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from abc import ABC, abstractmethod
2+
from typing import List, Union, Tuple, Dict, Any
3+
import numpy as np
4+
from epde.structure.main_structures import Equation, SoEq
5+
6+
class BaseSolverAdapter(ABC):
7+
"""Базовый класс для всех адаптеров солверов."""
8+
9+
@abstractmethod
10+
def solve(self, equation_or_system: Union[Equation, SoEq],
11+
grids: List[np.ndarray],
12+
data: Union[np.ndarray, List[np.ndarray]]) -> Tuple[List[np.ndarray], float]:
13+
"""
14+
Решает уравнение/систему на заданных сетках.
15+
16+
Parameters
17+
----------
18+
equation_or_system : Equation or SoEq
19+
Одиночное уравнение или система.
20+
grids : list of np.ndarray
21+
Сетки координат (каждая размерность – отдельный массив).
22+
data : np.ndarray or list of np.ndarray
23+
Эталонные значения (для каждой переменной) для расчёта ошибки.
24+
25+
Returns
26+
-------
27+
solutions : list of np.ndarray
28+
Список решений (по одному массиву на переменную).
29+
loss : float
30+
Числовая метрика ошибки (например, RMSE по всей сетке).
31+
"""
32+
pass
33+
34+
@abstractmethod
35+
def get_requirements(self) -> Dict[str, Any]:
36+
"""
37+
Возвращает требования солвера к представлению уравнений.
38+
39+
Returns
40+
-------
41+
dict
42+
Ключи:
43+
- 'form': 'explicit_ode', 'pde_residual', 'system_of_odes'
44+
- 'needs_initial_conditions': bool
45+
- 'needs_boundary_conditions': bool
46+
- 'supports_multisample': bool
47+
"""
48+
pass
49+
50+
def supports_multisample(self) -> bool:
51+
return self.get_requirements().get('supports_multisample', False)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import numpy as np
2+
from scipy.integrate import solve_ivp
3+
from epde.solver.base import BaseSolverAdapter
4+
from epde.solver.ode_converter import ODEToFirstOrder
5+
from epde.structure.main_structures import Equation, SoEq
6+
import epde.globals as global_var
7+
import sympy as sp
8+
9+
10+
class ClassicalODEAdapter(BaseSolverAdapter):
11+
def __init__(self, **config):
12+
self.method = config.get('method', 'RK45')
13+
self.rtol = config.get('rtol', 1e-6)
14+
self.atol = config.get('atol', 1e-9)
15+
self.rhs = config.get('rhs')
16+
self.y0 = config.get('y0')
17+
self._auto_rhs = self.rhs is None
18+
self._converter = ODEToFirstOrder() if self._auto_rhs else None
19+
20+
def _get_order(self, equation):
21+
if not self._auto_rhs:
22+
return None
23+
expr = self._converter._build_expression(equation, equation.main_var_to_explain)
24+
u = sp.Function(equation.main_var_to_explain)(self._converter.t)
25+
max_order = 0
26+
for term in sp.preorder_traversal(expr):
27+
if isinstance(term, sp.Derivative) and term.args[0] == u:
28+
order = term.args[1][1]
29+
if order > max_order:
30+
max_order = order
31+
return max_order
32+
33+
def _get_initial_conditions(self, equation, data):
34+
if self.y0 is not None:
35+
return self.y0
36+
order = self._get_order(equation)
37+
if order == 1:
38+
return [data[0]]
39+
else:
40+
raise ValueError(f"Для уравнения порядка {order} необходимо явно задать y0 в конфигурации.")
41+
42+
def solve(self, equation_or_system, grids, data):
43+
print("[DEBUG] ClassicalODEAdapter.solve called")
44+
print(f"[DEBUG] equation_or_system type: {type(equation_or_system)}")
45+
print(f"[DEBUG] data type: {type(data)}")
46+
if isinstance(data, (list, tuple)):
47+
print(f"[DEBUG] data length: {len(data)}")
48+
for i, d in enumerate(data):
49+
print(f"[DEBUG] data[{i}].shape: {d.shape}")
50+
else:
51+
print(f"[DEBUG] data.shape: {data.shape}")
52+
53+
if len(grids) != 1:
54+
raise ValueError("ClassicalODEAdapter работает только с 1D временной сеткой.")
55+
mask = global_var.grid_cache.g_func_mask
56+
t_full = grids[0].flatten()
57+
t_masked = t_full[mask]
58+
t_span = (t_masked.min(), t_masked.max())
59+
t_eval = t_masked
60+
61+
# Определяем правую часть и начальные условия
62+
if isinstance(equation_or_system, Equation):
63+
if self.rhs is not None:
64+
rhs = self.rhs
65+
y0 = self.y0
66+
if y0 is None:
67+
raise ValueError("Для явной rhs необходимо указать y0.")
68+
else:
69+
rhs = self._converter.equation_to_rhs(equation_or_system, equation_or_system.main_var_to_explain)
70+
y0 = self._get_initial_conditions(equation_or_system, data)
71+
elif isinstance(equation_or_system, SoEq):
72+
# Для системы: rhs и y0 должны быть заданы явно
73+
if self.rhs is None:
74+
raise NotImplementedError(
75+
"Автоматическое преобразование систем ОДУ пока не поддерживается. Укажите rhs в конфигурации.")
76+
rhs = self.rhs
77+
y0 = self.y0
78+
if y0 is None:
79+
raise ValueError("Для системы необходимо указать y0 в конфигурации.")
80+
else:
81+
raise TypeError("Unsupported equation type")
82+
83+
sol = solve_ivp(rhs, t_span, y0, method=self.method,
84+
t_eval=t_eval, rtol=self.rtol, atol=self.atol)
85+
if not sol.success:
86+
raise RuntimeError(f"ODE solver failed: {sol.message}")
87+
88+
solutions = [sol.y[i] for i in range(sol.y.shape[0])]
89+
90+
print(f"[DEBUG] solutions length: {len(solutions)}")
91+
for i, s in enumerate(solutions):
92+
print(f"[DEBUG] solutions[{i}].shape: {s.shape}")
93+
# data может быть списком массивов (для SoEq) или одним массивом (для Equation)
94+
if isinstance(data, (list, tuple)):
95+
loss = np.mean([np.sqrt(np.mean((solutions[i] - data[i]) ** 2)) for i in range(len(solutions))])
96+
else:
97+
loss = np.sqrt(np.mean((solutions[0] - data) ** 2))
98+
return solutions, loss
99+
100+
def get_requirements(self):
101+
return {
102+
'form': 'explicit_ode',
103+
'needs_initial_conditions': True,
104+
'needs_boundary_conditions': False,
105+
'supports_multisample': False
106+
}

epde/solver/factory.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from epde.solver.base import BaseSolverAdapter
2+
from epde.solver.unified_deepxde_adapter import UnifiedDeepXDEAdapter
3+
from epde.solver.classical_ode_adapter import ClassicalODEAdapter
4+
5+
class SolverFactory:
6+
_registry = {
7+
'deepxde': UnifiedDeepXDEAdapter,
8+
'classical_ode': ClassicalODEAdapter,
9+
}
10+
11+
@classmethod
12+
def register(cls, name: str, adapter_class):
13+
"""Регистрирует новый тип солвера."""
14+
cls._registry[name] = adapter_class
15+
16+
@classmethod
17+
def create(cls, solver_type: str, **config) -> BaseSolverAdapter:
18+
"""Создаёт экземпляр адаптера по имени."""
19+
if solver_type not in cls._registry:
20+
raise ValueError(f"Неизвестный тип солвера: {solver_type}. Доступны: {list(cls._registry.keys())}")
21+
return cls._registry[solver_type](**config)

epde/solver/ode_converter.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import sympy as sp
2+
import numpy as np
3+
from typing import Callable
4+
from epde.structure.main_structures import Equation
5+
6+
class ODEToFirstOrder:
7+
def __init__(self, t_symbol: str = 't'):
8+
self.t = sp.Symbol(t_symbol, real=True)
9+
self.debug = True # можно выключить после отладки
10+
11+
def _build_expression(self, eq: Equation, var_name: str) -> sp.Expr:
12+
u = sp.Function(var_name)(self.t)
13+
expr = 0
14+
if self.debug:
15+
print(f"[DEBUG] Building expression for equation, target_idx={eq.target_idx}")
16+
for term_idx, term in enumerate(eq.structure):
17+
coeff = eq.weights_final[term_idx] if term_idx < len(eq.weights_final) else 1.0
18+
term_expr = 1
19+
if self.debug:
20+
print(f" Term {term_idx}: {term.name}, coeff={coeff}")
21+
for factor in term.structure:
22+
deriv_code = getattr(factor, "deriv_code", None)
23+
is_deriv = factor.is_deriv and deriv_code is not None and len(deriv_code) > 0 and not all(v is None for v in deriv_code)
24+
if self.debug:
25+
print(f" Factor: is_deriv={factor.is_deriv}, variable={getattr(factor, 'variable', None)}, deriv_code={deriv_code}, params={factor.params}, is_deriv_flag={is_deriv}")
26+
if is_deriv:
27+
# Настоящая производная (deriv_code содержит числа, не None)
28+
order = len(deriv_code)
29+
deriv = sp.Derivative(u, (self.t, order))
30+
power = factor.params[-1] if factor.params else 1.0
31+
term_expr *= deriv ** power
32+
if self.debug:
33+
print(f" Derivative order {order}, power {power}, deriv={deriv}")
34+
else:
35+
# Не производная – переменная или константа
36+
if hasattr(factor, 'variable') and factor.variable is not None:
37+
power = factor.params[-1] if factor.params else 1.0
38+
term_expr *= u ** power
39+
if self.debug:
40+
print(f" Variable {factor.variable}, power {power}")
41+
else:
42+
if hasattr(factor, 'params') and len(factor.params) > 0:
43+
term_expr *= sp.Float(factor.params[-1])
44+
if self.debug:
45+
print(f" Constant {factor.params[-1]}")
46+
else:
47+
if self.debug:
48+
print(f" Unknown factor, ignored")
49+
if term_idx == eq.target_idx:
50+
expr -= coeff * term_expr
51+
if self.debug:
52+
print(f" Target term, adding -{coeff} * {term_expr}")
53+
else:
54+
expr += coeff * term_expr
55+
if self.debug:
56+
print(f" Adding +{coeff} * {term_expr}")
57+
if self.debug:
58+
print(f" Total expression: {expr}")
59+
return expr
60+
61+
def equation_to_rhs(self, eq: Equation, var_name: str) -> Callable:
62+
expr = self._build_expression(eq, var_name)
63+
u = sp.Function(var_name)(self.t)
64+
65+
derivs = [arg for arg in sp.preorder_traversal(expr) if isinstance(arg, sp.Derivative) and arg.args[0] == u]
66+
if not derivs:
67+
raise ValueError("Нет производных")
68+
max_order = max(d.args[1][1] for d in derivs)
69+
if self.debug:
70+
print(f"[DEBUG] max_order = {max_order}")
71+
72+
u_deriv = sp.Derivative(u, (self.t, max_order))
73+
sol = sp.solve(expr, u_deriv)
74+
if self.debug:
75+
print(f"[DEBUG] sol = {sol}")
76+
if not sol:
77+
raise ValueError(f"Не удалось выразить {u_deriv} из уравнения {expr}")
78+
rhs_expr = sol[0]
79+
80+
y_sym = [sp.Symbol(f'{var_name}_{i}', real=True) for i in range(max_order)]
81+
subs = {u: y_sym[0]}
82+
for i in range(1, max_order):
83+
subs[sp.Derivative(u, (self.t, i))] = y_sym[i]
84+
rhs_expr = rhs_expr.subs(subs)
85+
if self.debug:
86+
print(f"[DEBUG] rhs_expr after substitution = {rhs_expr}")
87+
88+
rhs_expr = rhs_expr.replace(lambda x: isinstance(x, sp.Derivative), lambda x: 0)
89+
rhs_expr = sp.simplify(rhs_expr)
90+
if self.debug:
91+
print(f"[DEBUG] rhs_expr after derivative replacement = {rhs_expr}")
92+
93+
derivatives = [y_sym[i+1] if i+1 < max_order else rhs_expr for i in range(max_order)]
94+
if self.debug:
95+
print(f"[DEBUG] derivatives = {derivatives}")
96+
97+
rhs_func_args = sp.lambdify([self.t] + y_sym, derivatives, modules='numpy')
98+
def rhs_wrapper(t, y):
99+
if self.debug:
100+
print(f"[DEBUG] rhs_wrapper t={t}, y={y}")
101+
res = rhs_func_args(t, *y)
102+
if self.debug:
103+
print(f"[DEBUG] rhs_wrapper res = {res}")
104+
return np.array(res, dtype=float)
105+
return rhs_wrapper
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from epde.solver.base import BaseSolverAdapter
2+
from epde.solver.factory import SolverFactory
3+
from epde.solver.unified_deepxde_adapter import DeepXDEAdapter
4+
from epde.solver.classical_ode_adapter import ClassicalODEAdapter
5+
6+
__all__ = [
7+
'BaseSolverAdapter',
8+
'SolverFactory',
9+
'DeepXDEAdapter',
10+
'ClassicalODEAdapter',
11+
]
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import numpy as np
2+
import copy
3+
from scipy.integrate import solve_ivp
4+
from epde.interface.interface import EpdeSearch
5+
from epde.interface.equation_translator import translate_equation
6+
from epde.structure.main_structures import SoEq, Chromosome
7+
from epde.interface.token_family import TFPool
8+
from epde.operators.utils.default_parameter_loader import EvolutionaryParams
9+
from epde.solver.factory import SolverFactory
10+
11+
def create_equation_from_str(eq_str, target_var, base_pool, all_vars):
12+
families_copy = [copy.deepcopy(fam) for fam in base_pool.families]
13+
for fam in families_copy:
14+
if hasattr(fam, 'variable') and fam.variable is not None and fam.variable != target_var:
15+
fam.status['demands_equation'] = False
16+
temp_pool = TFPool(families_copy)
17+
soeq = translate_equation(eq_str, temp_pool, all_vars=[target_var])
18+
return soeq.vals[target_var]
19+
20+
def lv_rhs(t, y):
21+
u, v = y
22+
alpha, beta, gamma, delta = 2/3, 4/3, 1.0, 1.0
23+
du = alpha * u - beta * u * v
24+
dv = delta * u * v - gamma * v
25+
return [du, dv]
26+
27+
t = np.linspace(0, 20, 200)
28+
sol_ref = solve_ivp(lv_rhs, (0,20), [1.0,1.0], t_eval=t, method='RK45', rtol=1e-6, atol=1e-9)
29+
exact_u, exact_v = sol_ref.y
30+
31+
data_u = exact_u + 0.01 * np.random.normal(size=exact_u.shape)
32+
data_v = exact_v + 0.01 * np.random.normal(size=exact_v.shape)
33+
34+
search = EpdeSearch(
35+
use_solver=False,
36+
multiobjective_mode=True,
37+
coordinate_tensors=[t],
38+
verbose_params={'show_iter_idx': False},
39+
device='cpu'
40+
)
41+
search.set_preprocessor(default_preprocessor_type='FD', preprocessor_kwargs={})
42+
search.create_pool(data=[data_u, data_v], variable_names=['u', 'v'], max_deriv_order=1, additional_tokens=[])
43+
44+
correct_eqs = [
45+
'0.6666666666666666 * u{power: 1.0} + -1.3333333333333333 * u{power: 1.0} * v{power: 1.0} = du/dx0{power: 1.0}',
46+
'1.0 * u{power: 1.0} * v{power: 1.0} + -1.0 * v{power: 1.0} = dv/dx0{power: 1.0}'
47+
]
48+
49+
eq_u = create_equation_from_str(correct_eqs[0], 'u', search.pool, ['u', 'v'])
50+
eq_u.main_var_to_explain = 'u'
51+
eq_u.weights_internal = np.ones(len(eq_u.structure) - 1)
52+
eq_u.weights_internal_evald = True
53+
eq_u.weights_final_evald = True
54+
55+
eq_v = create_equation_from_str(correct_eqs[1], 'v', search.pool, ['u', 'v'])
56+
eq_v.main_var_to_explain = 'v'
57+
eq_v.weights_internal = np.ones(len(eq_v.structure) - 1)
58+
eq_v.weights_internal_evald = True
59+
eq_v.weights_final_evald = True
60+
61+
system = SoEq(search.pool, {})
62+
system.vals = Chromosome({'u': eq_u, 'v': eq_v}, {})
63+
system.moeadd_set = True
64+
65+
def solve_with_solver(solver_type, solver_config, system, t, data):
66+
adapter = SolverFactory.create(solver_type, **solver_config)
67+
solutions, loss = adapter.solve(system, [t], data)
68+
return solutions, loss
69+
70+
71+
print("=" * 50)
72+
print("Classical ODE solver (RK45)")
73+
print("=" * 50)
74+
75+
solver_config_classical = {
76+
"method": "RK45",
77+
"rtol": 1e-6,
78+
"atol": 1e-9,
79+
"rhs": lv_rhs,
80+
"y0": [1.0, 1.0]
81+
}
82+
solutions_cl, loss_cl = solve_with_solver("classical_ode", solver_config_classical, system, t, [data_u, data_v])
83+
print(f"Loss (RMSE): {loss_cl:.6f}")
84+
print(f"Max error u: {np.max(np.abs(solutions_cl[0] - exact_u)):.6f}")
85+
print(f"Max error v: {np.max(np.abs(solutions_cl[1] - exact_v)):.6f}")
86+
87+
print("\n" + "=" * 50)
88+
print("DeepXDE (PINN) solver")
89+
print("=" * 50)
90+
91+
solver_config_deepxde = EvolutionaryParams().get_default_params_for_operator('DeepXDEBasedFitness')
92+
solutions_dx, loss_dx = solve_with_solver("deepxde", solver_config_deepxde, system, t, [data_u, data_v])
93+
print(f"Loss (RMSE): {loss_dx:.6f}")
94+
print(f"Max error u: {np.max(np.abs(solutions_dx[0] - exact_u)):.6f}")
95+
print(f"Max error v: {np.max(np.abs(solutions_dx[1] - exact_v)):.6f}")

0 commit comments

Comments
 (0)