Skip to content

Commit e340bf4

Browse files
Added Unified solver (classical + DeepXDE)
1 parent 5c41e47 commit e340bf4

20 files changed

Lines changed: 529 additions & 79 deletions

epde/operators/common/fitness.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,10 @@
1313
import matplotlib.pyplot as plt
1414
from matplotlib import cm
1515

16-
from epde.integrate import SolverAdapter, DeepXDEAdapter
16+
from epde.integrate import SolverAdapter
1717
from epde.structure.main_structures import SoEq, Equation
1818
from epde.operators.utils.template import CompoundOperator
1919
import epde.globals as global_var
20-
from sklearn.linear_model import LinearRegression, Ridge
21-
from scipy.optimize import minimize
22-
from epde.supplementary import minmax_normalize
2320
from epde.supplementary import calculate_weights
2421

2522
LOSS_NAN_VAL = 1e7
@@ -391,13 +388,18 @@ class DeepXDEBasedFitness(CompoundOperator):
391388

392389
def __init__(self, param_keys: list):
393390
super().__init__(param_keys)
394-
self.adapter = None
395-
396-
def set_adapter(self, config: dict = None, pretrained_net=None):
397-
if self.adapter is None:
398-
from epde.integrate.deepxde_integration import DeepXDEAdapter
399-
cfg = self.params.get('deepxde_config', {}) if config is None else config
400-
self.adapter = DeepXDEAdapter(pretrained_net=pretrained_net, **cfg)
391+
self._solver = None
392+
393+
def _get_solver(self):
394+
if self._solver is None:
395+
from epde.solver.factory import SolverFactory
396+
solver_type = self.params.get('solver_type', 'deepxde')
397+
solver_config = self.params.get('solver_config', {})
398+
# Обратная совместимость со старым параметром deepxde_config
399+
if 'deepxde_config' in self.params and solver_type == 'deepxde':
400+
solver_config = self.params['deepxde_config']
401+
self._solver = SolverFactory.create(solver_type, **solver_config)
402+
return self._solver
401403

402404
def apply(self, objective, arguments: dict, force_out_of_place: bool = False):
403405
self_args, subop_args = self.parse_suboperator_args(arguments=arguments)
@@ -406,13 +408,7 @@ def apply(self, objective, arguments: dict, force_out_of_place: bool = False):
406408
self.suboperators['sparsity'].apply(objective, subop_args.get('sparsity', {}))
407409
self.suboperators['coeff_calc'].apply(objective, subop_args.get('coeff_calc', {}))
408410

409-
try:
410-
pretrained_net = deepcopy(global_var.solution_guess_nn)
411-
except:
412-
pretrained_net = None
413-
self.set_adapter(pretrained_net=pretrained_net)
414-
415-
keys, grids = global_var.grid_cache.get_all(mode='numpy')
411+
keys, grids = global_var.grid_cache.get_all(mode='numpy', structural=True)
416412

417413
if isinstance(objective, SoEq):
418414
data_list = []
@@ -424,15 +420,26 @@ def apply(self, objective, arguments: dict, force_out_of_place: bool = False):
424420
_, target, _ = objective.evaluate(normalize=False, return_val=False)
425421
data_list = [target.reshape(-1)]
426422

423+
solver = self._get_solver()
424+
425+
print(f"[DEBUG] objective type: {type(objective)}")
426+
if isinstance(objective, SoEq):
427+
print(f"[DEBUG] vars_to_describe: {objective.vars_to_describe}")
428+
print(f"[DEBUG] data_list length: {len(data_list)}")
429+
for i, d in enumerate(data_list):
430+
print(f"[DEBUG] data_list[{i}].shape: {d.shape}")
431+
427432
try:
428-
solution_list, loss = self.adapter.solve(equation_or_system=objective,
429-
grids=grids,
430-
data=data_list)
433+
solution_list, loss = solver.solve(equation_or_system=objective,
434+
grids=grids,
435+
data=data_list)
431436
if np.isnan(loss):
432437
raise ValueError("NaN loss")
433438

434439
if isinstance(objective, SoEq):
435-
for idx, (var_name, eq) in enumerate({val: objective.vals[val] for val in objective.vars_to_describe}.items()):
440+
# Перебираем уравнения в порядке vars_to_describe
441+
for idx, var_name in enumerate(objective.vars_to_describe):
442+
eq = objective.vals[var_name]
436443
err = self._compute_error(solution_list[idx], data_list[idx], eq)
437444
if force_out_of_place:
438445
pass
@@ -451,7 +458,9 @@ def apply(self, objective, arguments: dict, force_out_of_place: bool = False):
451458
objective.fitness_calculated = True
452459
self._compute_stability_for_equation(objective)
453460
except Exception as e:
454-
print(f'[DeepXDEBasedFitness] DeepXDE solve failed: {e}')
461+
print(f'[DeepXDEBasedFitness] Solver failed: {e}')
462+
import traceback
463+
traceback.print_exc()
455464
fitness_value = 1e7
456465
if force_out_of_place:
457466
return fitness_value
@@ -483,7 +492,6 @@ def _compute_error(self, solution, data, eq):
483492
return err
484493

485494
def _compute_stability_for_equation(self, eq: Equation):
486-
# Повторно вычисляется evaluate
487495
_, target, features = eq.evaluate(normalize=False, return_val=False)
488496
data_shape = global_var.grid_cache.inner_shape
489497
self.get_g_fun_vals()

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

0 commit comments

Comments
 (0)