Skip to content

Commit 88f6d6d

Browse files
Merge branch 'ITMO-NSS-team:main' into main
2 parents 921b5a2 + a3f8700 commit 88f6d6d

82 files changed

Lines changed: 4066 additions & 708 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,8 @@ dmypy.json
130130
.pyre/
131131
#cache
132132
/cache/*.tar
133+
134+
# Thesis run outputs (regenerated by projects/thesis/run.py + aggregators)
135+
projects/thesis/results/
136+
projects/thesis/thesis_summary.json
137+
projects/thesis/thesis_ablation_summary.json

epde/eq_mo_objectives.py

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -60,37 +60,8 @@ def equation_complexity_by_terms(system, equation_key):
6060
return np.count_nonzero(system.vals[equation_key].weights_internal)
6161

6262

63-
def equation_complexity_by_factors(system, equation_key):
64-
'''
65-
Evaluate the complexity of the system of PDEs, evaluating a number of factors in terms for each
66-
equation. In the evaluation, we consider only terms with non-zero weights and target, while
67-
the free coefficient is not included in the final metric. Also, the real-valued factors are
68-
not considered in the result.
69-
70-
Parameters:
71-
-----------
72-
system - ``epde.structure.main_structures.SoEq`` object
73-
The system, that is to be evaluated.
74-
75-
Returns:
76-
----------
77-
discrepancy : list of integers.
78-
The values of the error metric: list entry for each of the equations.
79-
'''
80-
# eq_compl = 0
81-
82-
# for idx, term in enumerate(system.vals[equation_key].structure):
83-
# if idx < system.vals[equation_key].target_idx:
84-
# if not system.vals[equation_key].weights_final[idx] == 0:
85-
# eq_compl += len(term.structure)
86-
# elif idx > system.vals[equation_key].target_idx:
87-
# if not system.vals[equation_key].weights_final[idx-1] == 0:
88-
# eq_compl += len(term.structure)
89-
# else:
90-
# eq_compl += len(term.structure)
91-
# return eq_compl
63+
def _complexity_single_eq(system, equation_key):
9264
eq_compl = 0
93-
9465
for idx, term in enumerate(system.vals[equation_key].structure):
9566
if idx < system.vals[equation_key].target_idx:
9667
if not system.vals[equation_key].weights_final[idx] == 0:
@@ -103,6 +74,19 @@ def equation_complexity_by_factors(system, equation_key):
10374
return eq_compl
10475

10576

77+
def equation_complexity_by_factors(system, equation_key=None):
78+
'''
79+
Evaluate the complexity of the system of PDEs as a number of factors in
80+
non-zero terms for each equation, excluding the free coefficient and
81+
real-valued factors. When ``equation_key`` is None, returns a per-equation
82+
tuple matching the ``system.vars_to_describe`` order; otherwise the scalar
83+
complexity for the named equation.
84+
'''
85+
if equation_key is None:
86+
return tuple(_complexity_single_eq(system, k) for k in system.vars_to_describe)
87+
return _complexity_single_eq(system, equation_key)
88+
89+
10690
def equation_terms_stability(system, equation_key = None):
10791
if equation_key:
10892
assert system.vals[equation_key].stability_calculated

epde/integrate/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,14 @@
22
from .bop import BOPElement, BoundaryConditions
33
from .pinn_integration import SolverAdapter
44
from .numeric_integration import OdeintAdapter
5-
from .deepxde_integration import DeepXDEAdapter
5+
6+
7+
# ``deepxde_integration`` does ``import deepxde``, which prints a backend
8+
# banner on first load. Defer that until the DeepXDE adapter is actually
9+
# requested so plain ``import epde`` / ``from epde.integrate import
10+
# SolverAdapter`` stays quiet.
11+
def __getattr__(name):
12+
if name == 'DeepXDEAdapter':
13+
from .deepxde_integration import DeepXDEAdapter
14+
return DeepXDEAdapter
15+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

epde/integrate/deepxde_integration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ def __init__(self, pretrained_net=None, **config):
261261
self.num_boundary = int(self.config.get('num_boundary', 500))
262262
self.num_initial = int(self.config.get('num_initial', 500))
263263
self.epochs = int(self.config.get('epochs', 10000))
264+
# self.iterations = int(self.config.get('epochs', 5))
264265
self.bc_type = self.config.get('bc_type', 'Dirichlet')
265266
self.fallback_bc_value = self.config.get('fallback_bc_value', 0.0)
266267

epde/interface/interface.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,16 @@ class EpdeSearch(object):
230230
optimizer_exec_params (`dict`): parameters for execution algorithm of optimization
231231
optimizer (`OptimizationPatternDirector`): the strategy of the evolutionary algorithm
232232
"""
233-
def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default_strategy: bool = True, director=None,
233+
def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default_strategy: bool = True, director=None,
234234
director_params: dict = {'variation_params': {}, 'mutation_params': {},
235-
'pareto_combiner_params': {}, 'pareto_updater_params': {}},
235+
'pareto_combiner_params': {}, 'pareto_updater_params': {}},
236236
time_axis: int = 0, define_domain: bool = True, function_form=None, boundary: int = 0,
237-
use_solver: bool = False, verbose_params: dict = {'show_iter_idx' : True},
237+
use_solver: bool = False, verbose_params: dict = {'show_iter_idx' : True},
238238
coordinate_tensors=None, memory_for_cache=15, prune_domain: bool = False,
239-
pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2,
240-
division_fractions=3, rectangular: bool = True,
241-
params_filename: str = None, device: str = 'cpu'):
239+
pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2,
240+
division_fractions=3, rectangular: bool = True,
241+
params_filename: str = None, device: str = 'cpu',
242+
fitness_cls=None, sparsity_cls=None):
242243
"""
243244
Args:
244245
multiobjective_mode (`bool`): optional, default True
@@ -319,8 +320,9 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default
319320
self.director = BaselineDirector()
320321
builder = StrategyBuilder(EvolutionaryStrategy)
321322
self.director.builder = builder
322-
self.director.use_baseline(use_solver=self._mode_info['solver_fitness'],
323-
use_pic=self._use_pic, params=director_params)
323+
self.director.use_baseline(use_solver=self._mode_info['solver_fitness'],
324+
use_pic=self._use_pic, params=director_params,
325+
fitness_cls=fitness_cls, sparsity_cls=sparsity_cls)
324326
else:
325327
raise NotImplementedError('Wrong arguments passed during the epde search initialization')
326328

@@ -360,8 +362,9 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {}
360362
subregion_mating_limitation: float = .95,
361363
PBI_penalty: float = 1., training_epochs: int = 100,
362364
neighborhood_selector: Callable = simple_selector,
363-
neighborhood_selector_params: tuple = (4,)):
364-
"""
365+
neighborhood_selector_params: tuple = (4,),
366+
early_stopping_callback: Callable = None):
367+
r"""
365368
Setting the parameters of the multiobjective evolutionary algorithm. declaration of
366369
the default values is held in the initialization of EpdeSearch object.
367370
@@ -416,9 +419,10 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {}
416419
'nds_method' : nds_method,
417420
'ndl_update' : ndl_update_method}
418421

419-
self.optimizer_exec_params = {'epochs' : training_epochs}
420-
421-
def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {},
422+
self.optimizer_exec_params = {'epochs' : training_epochs,
423+
'early_stopping_callback' : early_stopping_callback}
424+
425+
def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {},
422426
sorting_method: Callable = simple_sorting, training_epochs: int = 50):
423427
"""
424428
Setting parameters for singelobjective optimization.
@@ -949,6 +953,18 @@ def cache(self):
949953
else:
950954
return None, global_var.tensor_cache
951955

956+
@property
957+
def pareto_history(self):
958+
"""Per-epoch Pareto-level-0 snapshots, populated during ``fit``.
959+
960+
Returns a list of length ``training_epochs``; each element is a
961+
list of ``{'text_form': str, 'obj_fun': list}`` dicts -- one per
962+
solution on the non-dominated front at the end of that epoch.
963+
Empty list when the optimizer hasn't been run or doesn't track
964+
epoch history (e.g. single-objective mode).
965+
"""
966+
return getattr(self.optimizer, '_pareto_history', [])
967+
952968
def get_equations_by_complexity(self, complexity : Union[float, list]):
953969
'''
954970
Get equations with desired complexity. Works best with ``EpdeSearch.visualize_solutions(...)``

epde/interface/token_family.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -557,15 +557,26 @@ def create_with_var(self, variable: str, token_status=None, **kwargs):
557557
assert variable is not None, 'Can not create token with a specific variable for '
558558
families = [f for f in self.families if variable == f.variable]
559559

560-
while True:
560+
max_iter = len(families) + 1
561+
family = None
562+
for _ in range(max_iter):
563+
if not families:
564+
raise RuntimeError(
565+
f"TFPool.create_with_var: no family can produce a token for variable={variable!r}"
566+
)
561567
try:
562568
probabilities = np.array([len(f.tokens) for f in families])
563-
family = np.random.choice(families, p = probabilities/probabilities.sum())
564-
return family.create(label=None, token_status=token_status,
565-
all_vars = [family.variable for family in self.families_demand_equation],
569+
family = np.random.choice(families, p=probabilities/probabilities.sum())
570+
return family.create(label=None, token_status=token_status,
571+
all_vars=[fam.variable for fam in self.families_demand_equation],
566572
**kwargs)
567573
except ValueError:
568-
families.remove(family)
574+
if family is not None and family in families:
575+
families.remove(family)
576+
family = None
577+
raise RuntimeError(
578+
f"TFPool.create_with_var: exhausted {max_iter} attempts for variable={variable!r}"
579+
)
569580

570581
def __add__(self, other):
571582
return TFPool(families=self.families + other.families)

epde/operators/common/fitness.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +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
17+
# DeepXDEAdapter is imported lazily inside DeepXDEBasedFitness.apply() to
18+
# avoid triggering deepxde's import-time backend banner when no DeepXDE
19+
# solver is used (e.g. legacy L2/L2LR fitness paths).
1720
from epde.structure.main_structures import SoEq, Equation
1821
from epde.operators.utils.template import CompoundOperator
1922
import epde.globals as global_var
@@ -69,17 +72,38 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
6972

7073
if force_out_of_place:
7174
self.suboperators['sparsity'].apply(objective, subop_args['sparsity'])
75+
# Reject degenerate candidates whose entire non-target library was
76+
# zeroed by sparsity. Without this, ``EqRightPartSelector`` may
77+
# commit a target_idx whose only surviving content is the
78+
# intercept, yielding population members of the form
79+
# ``~0 = u^2 * du/dx0`` (no real LHS) that cannot represent any
80+
# PDE by construction. Mirrors the rejection in ``L2LRFitness``
81+
# so the LEGACY (L2Fitness) and NEW (L2LRFitness) RPS sweeps
82+
# share the same admissibility criterion.
83+
if all(objective.weights_internal == 0):
84+
return None
7285
self.suboperators['coeff_calc'].apply(objective, subop_args['coeff_calc'])
7386

7487
_, target, features = objective.evaluate(normalize = False, return_val = False)
7588
if features is None:
7689
discr_feats = 0
7790
else:
78-
discr_feats = np.dot(features, objective.weights_final[:-1][objective.weights_internal != 0])
91+
n_cols = features.shape[1] if features.ndim > 1 else 1
92+
mask = objective.weights_internal != 0
93+
if n_cols == len(mask):
94+
discr_feats = np.dot(features, objective.weights_internal)
95+
elif n_cols == int(mask.sum()):
96+
discr_feats = np.dot(features, objective.weights_final[:-1])
97+
else:
98+
discr_feats = np.zeros(features.shape[0])
7999

80100
discr = (discr_feats + np.full(target.shape, objective.weights_final[-1]) - target)
81-
self.g_fun_vals = global_var.grid_cache.g_func_flat
82-
discr = np.multiply(discr, self.g_fun_vals)
101+
try:
102+
self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func_mask].reshape(-1)
103+
except AttributeError:
104+
self.g_fun_vals = None
105+
if self.g_fun_vals is not None and self.g_fun_vals.shape == discr.shape:
106+
discr = np.multiply(discr, self.g_fun_vals)
83107
rl_error = np.linalg.norm(discr, ord = 2)
84108

85109
if not (self.params['penalty_coeff'] > 0. and self.params['penalty_coeff'] < 1.):
@@ -137,7 +161,21 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
137161
if features is None:
138162
discr = target - target.mean()
139163
else:
140-
discr_feats = np.dot(features, objective.weights_final[:-1])
164+
# ``features`` width depends on the ``normalize`` flag passed to
165+
# ``evaluate`` above: ``normalize=True`` returns all N-1
166+
# non-target columns; ``normalize=False`` filters to only the
167+
# nonzero-weight columns. ``weights_final[:-1]`` matches the
168+
# latter shape (nonzero count); ``weights_internal`` matches the
169+
# former (full N-1, with zeros). Pick whichever lines up with
170+
# the actual feature matrix -- same pattern as L2Fitness.apply.
171+
n_cols = features.shape[1] if features.ndim > 1 else 1
172+
mask = objective.weights_internal != 0
173+
if n_cols == len(mask):
174+
discr_feats = np.dot(features, objective.weights_internal)
175+
elif n_cols == int(mask.sum()):
176+
discr_feats = np.dot(features, objective.weights_final[:-1])
177+
else:
178+
discr_feats = np.zeros(features.shape[0])
141179
discr_feats = discr_feats + objective.weights_final[-1]
142180
discr = target - discr_feats
143181

@@ -155,19 +193,23 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
155193
objective.aic_calculated = True
156194

157195
data_shape = global_var.grid_cache.inner_shape
158-
if hasattr(objective, '_cached_sw_weights') and objective._cached_sw_weights is not None:
159-
weights = objective._cached_sw_weights
196+
if features is None:
197+
# Degenerate candidate (all features pruned by sparsity).
198+
# Nothing to fit sliding-window weights on -- skip the CV
199+
# calculation and report unit stability so downstream callers
200+
# still get a finite value.
201+
total_lr = 1.0
160202
else:
161-
weights = calculate_weights(features, target, self.g_fun_vals, data_shape, objective.weights_final[-1] != 0)
162-
weights_arr = np.array(weights)
163-
std = weights_arr.std(axis=0, ddof=1)
164-
mu = weights_arr.mean(axis=0)
165-
166-
# Safe division
167-
with np.errstate(divide='ignore', invalid='ignore'):
168-
cv = (std ** 2) / (mu ** 2)
169-
170-
total_lr = sum(cv) / len(data_shape)
203+
if hasattr(objective, '_cached_sw_weights') and objective._cached_sw_weights is not None:
204+
weights = objective._cached_sw_weights
205+
else:
206+
weights = calculate_weights(features, target, self.g_fun_vals, data_shape, objective.weights_final[-1] != 0)
207+
weights_arr = np.array(weights)
208+
std = weights_arr.std(axis=0, ddof=1)
209+
mu = weights_arr.mean(axis=0)
210+
with np.errstate(divide='ignore', invalid='ignore'):
211+
cv = (std ** 2) / (mu ** 2)
212+
total_lr = sum(cv) / len(data_shape)
171213

172214
if force_out_of_place:
173215
return fitness_value * total_lr
@@ -357,9 +399,8 @@ def apply(self, objective: SoEq, arguments: dict, force_out_of_place: bool = Fal
357399
# Safe division
358400
with np.errstate(divide='ignore', invalid='ignore'):
359401
cv = (std ** 2) / (mu ** 2)
360-
cv[mu == 0] = 0.0 # Handle zero mean
361402

362-
total_lr = sum(cv[:-1]) / len(data_shape)
403+
total_lr = sum(cv) / len(data_shape)
363404

364405
eq.fitness_calculated = True
365406
eq.fitness_value = lp
@@ -491,8 +532,8 @@ def _compute_stability_for_equation(self, eq: Equation):
491532
weights_arr = np.array(weights)
492533
std = weights_arr.std(axis=0, ddof=1)
493534
mu = weights_arr.mean(axis=0)
494-
cv = np.where(mu != 0, (std / mu) ** 2, 0.0)
495-
total_lr = np.sum(cv[:-1]) / len(data_shape) if len(cv) > 1 else 0.0
535+
cv = (std ** 2) / (mu ** 2)
536+
total_lr = np.sum(cv) / len(data_shape)
496537
eq.coefficients_stability = total_lr
497538
eq.stability_calculated = True
498539

0 commit comments

Comments
 (0)