Skip to content

Commit c4521c4

Browse files
authored
Merge pull request #77 from Gromwud/main
Stability logic update, Operators logic refactor
2 parents 59f4e04 + e9f444f commit c4521c4

62 files changed

Lines changed: 1915 additions & 4700 deletions

Some content is hidden

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

epde/eq_mo_objectives.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def equation_fitness(system, equation_key = None):
3232
'''
3333
if equation_key:
3434
assert all(equation.fitness_calculated for equation in system.vals), 'Trying to call fitness before its evaluation.'
35-
res = system.vals[equation_key].fitness_calculated
35+
res = system.vals[equation_key].fitness_value
3636
else:
3737
for equation in system.vals:
3838
assert equation.fitness_calculated

epde/globals.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131
# ``PhysicsInformedLasso.get_cv``).
3232
gram_mode: str = 'vcoef'
3333

34+
# Which objective the SINGLE-objective optimizer minimises: 'discrepancy'
35+
# (default, the residual fitness) or 'instability' (vcoef coefficient
36+
# stability). The fitness host always computes discrepancy; when this is
37+
# 'instability' it ALSO computes instability and the objective reader
38+
# (SoEq.use_default_singleobjective_function) points the optimizer at it.
39+
# Set via ``set_single_objective_metric`` before ``build_search``.
40+
single_objective_metric: str = 'discrepancy'
41+
3442
# Per-rep seed for additive Gaussian noise applied at ``cfg.load_data()``;
3543
# rewritten each rep so every rep sees an independent noise realization.
3644
noise_seed = None
@@ -56,6 +64,15 @@
5664
# Default True; set False for the legacy joint mode solve.
5765
vc_mode_decouple: bool = True
5866

67+
# When True, ``PhysicsInformedLasso.fit`` (the 'max_corr' anchor mode) anchors
68+
# the L1 threshold to the WORKING RESIDUAL ``max_k|X_k^T r|`` (r = y minus the
69+
# previous outer-iteration fit) instead of the RAW target ``max_k|X_k^T y|``.
70+
# On the first pass r = y (full_coef_ = 0) so it matches the legacy anchor;
71+
# thereafter the scale tracks what is still UNEXPLAINED as RFE shrinks the
72+
# library, instead of staying pinned to ||y|| (which the dominant term inflates
73+
# and which masks weak terms). No effect in 'tstat' mode (no max_corr there).
74+
anchor_on_residual: bool = False
75+
5976

6077
def set_gram_config(mode: str = 'vcoef'):
6178
"""Override the global Gram-construction mode before ``build_search``.
@@ -74,6 +91,29 @@ def set_gram_config(mode: str = 'vcoef'):
7491
vc_modes_cache.clear()
7592

7693

94+
def set_single_objective_metric(metric: str = 'discrepancy'):
95+
"""Override the single-objective optimizer's objective before
96+
``build_search``. Mirrors ``set_gram_config``: a process-level global
97+
read at population construction by
98+
``SoEq.use_default_singleobjective_function`` and the single-objective
99+
director's fitness assembly.
100+
"""
101+
global single_objective_metric
102+
if metric not in ('discrepancy', 'instability'):
103+
raise ValueError(
104+
f'single_objective_metric must be "discrepancy" or "instability"; got {metric!r}')
105+
single_objective_metric = metric
106+
107+
108+
def set_anchor_on_residual(flag: bool = False):
109+
"""Override whether the 'max_corr' anchor uses the working residual
110+
(``max|X^T r|``) instead of the raw target (``max|X^T y|``), before
111+
``build_search``.
112+
"""
113+
global anchor_on_residual
114+
anchor_on_residual = bool(flag)
115+
116+
77117
def init_caches(set_grids: bool = False, device = 'cpu'):
78118
"""
79119
Initialization global variables for keeping input data, values of grid and useful tensors such as evaluated terms

epde/interface/interface.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
**EpdeSearch** class for main interactions between the user and the framework.
1212
1313
"""
14+
import inspect
1415
import pickle
1516
import numpy as np
1617
import torch
@@ -241,7 +242,7 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default
241242
pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2,
242243
division_fractions=3, rectangular: bool = True,
243244
params_filename: str = None, device: str = 'cpu',
244-
fitness_cls=None, sparsity_cls=None):
245+
discrepancy_metric: str = 'wape', sparsity_cls=None):
245246
"""
246247
Args:
247248
multiobjective_mode (`bool`): optional, default True
@@ -292,6 +293,7 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default
292293
self._device = device
293294
self.multiobjective_mode = multiobjective_mode
294295
self._use_pic = use_pic
296+
self._discrepancy_metric = discrepancy_metric
295297

296298
global_var.set_time_axis(time_axis)
297299
global_var.init_verbose(**verbose_params)
@@ -324,7 +326,7 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = True, use_default
324326
self.director.builder = builder
325327
self.director.use_baseline(use_solver=self._mode_info['solver_fitness'],
326328
use_pic=self._use_pic, params=director_params,
327-
fitness_cls=fitness_cls, sparsity_cls=sparsity_cls)
329+
discrepancy_metric=discrepancy_metric, sparsity_cls=sparsity_cls)
328330
else:
329331
raise NotImplementedError('Wrong arguments passed during the epde search initialization')
330332

@@ -361,8 +363,8 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {}
361363
H: int = 15, neighbors_number: int = 3,
362364
nds_method: Callable = fast_non_dominated_sorting,
363365
ndl_update_method: Callable = ndl_update,
364-
subregion_mating_limitation: float = .95,
365-
PBI_penalty: float = 1., training_epochs: int = 100,
366+
subregion_mating_limitation: float = .9,
367+
PBI_penalty: float = 5., training_epochs: int = 100,
366368
neighborhood_selector: Callable = simple_selector,
367369
neighborhood_selector_params: tuple = (4,),
368370
early_stopping_callback: Callable = None):
@@ -378,6 +380,10 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {}
378380
H (`float`): optional
379381
parameter of uniform spacing between the weight vectors; *H = 1 / delta*
380382
should be integer - a number of divisions along an objective coordinate axis.
383+
NOTE: currently ignored — the optimizer always uses *H = population_size - 1*,
384+
which (for the two-objective weight space used by EPDE) keeps the number of
385+
Das-Dennis weight vectors equal to the population size, as the MOEA/DD paper
386+
requires (N solutions <-> N weight vectors).
381387
neighbors_number (`int`): *> 0*, optional
382388
number of neighboring weight vectors to be considered during the operation
383389
of evolutionary operators as the "neighbors" of the processed sectors.
@@ -394,36 +400,47 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {}
394400
Dept. Electr. Comput. Eng., Michigan State Univ., East Lansing,
395401
MI, USA, Tech. Rep. COIN No. 2014014, 2014.*
396402
neighborhood_selector (`callable`): optional
397-
Method of finding "close neighbors" of the vector with proximity list.
398-
The baseline example of the selector, presented in
399-
``moeadd.moeadd_stc.simple_selector``, selects n-adjacent ones.
403+
DEPRECATED, ignored. Neighboring weight vectors are chosen by the
404+
``SimpleNeighborSelector`` operator, which randomly picks indices
405+
from the proximity list E(i), per Algorithm 3 of the MOEA/DD paper.
400406
subregion_mating_limitation (`float`): optional
401407
The probability of mating selection to be limited only to the selected
402-
subregions (adjacent to the weight vector domain).:math:`\delta \in [0., 1.)
408+
subregions (adjacent to the weight vector domain). :math:`\delta \in [0., 1.)`,
409+
default value is 0.9, as in the MOEA/DD paper.
403410
neighborhood_selector_params (`tuple|list`): optional
404-
Iterable, which will be passed into neighborhood_selector, as
405-
an arugument. *None*, is no additional arguments are required inside
406-
the selector.
411+
DEPRECATED, ignored (see ``neighborhood_selector``).
407412
training_epochs (`int`): optional
408413
Maximum number of iterations, during that the optimization will be held.
409414
Note, that if the algorithm converges to a single Pareto frontier,
410415
the optimization is stopped.
411416
PBI_penalty (`float`): optional
412-
The penalty parameter, used in penalty based intersection
413-
calculation, defalut value is 1.
417+
The penalty parameter :math:`\\theta`, used in penalty based intersection
418+
calculation, default value is 5.0, as in the MOEA/DD paper.
414419
415420
Returns:
416421
None
417422
"""
418423
self.optimizer_init_params = {'pop_size': population_size,
419424
'H': population_size-1, 'neighbors_number': neighbors_number,
420425
'solution_params': solution_params,
421-
'nds_method' : nds_method,
426+
'nds_method' : nds_method,
422427
'ndl_update' : ndl_update_method}
423-
428+
424429
self.optimizer_exec_params = {'epochs' : training_epochs,
425430
'early_stopping_callback' : early_stopping_callback}
426431

432+
# Forward the user-facing MOEA/DD parameters to the operators of the
433+
# strategy assembled in __init__ (previously these arguments were
434+
# accepted but silently dropped, so the JSON defaults always applied).
435+
director = getattr(self, 'director', None)
436+
if director is not None and director.builder is not None:
437+
blocks = director.builder.blocks_labeled
438+
if 'selection' in blocks:
439+
blocks['selection']._operator.params['delta'] = subregion_mating_limitation
440+
if 'pareto_updater_compl' in blocks:
441+
pareto_updater = blocks['pareto_updater_compl']._operator
442+
pareto_updater.suboperators['pareto_level_updater'].params['PBI_penalty'] = PBI_penalty
443+
427444
def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {},
428445
sorting_method: Callable = simple_sorting, training_epochs: int = 50):
429446
"""
@@ -851,7 +868,13 @@ def fit(self, data: Union[np.ndarray, list, tuple] = None, equation_terms_max_nu
851868
else:
852869
self.optimizer = optimizer
853870

854-
self.optimizer.optimize(**self.optimizer_exec_params)
871+
# Pass only the exec params this optimizer's ``optimize`` accepts:
872+
# SimpleOptimizer.optimize has no ``early_stopping_callback`` (a
873+
# MOEA/D-only exec param that set_moeadd_params leaves behind when an
874+
# EpdeSearch built multiobjective-by-default is run single-objective).
875+
_exec_keys = set(inspect.signature(self.optimizer.optimize).parameters) - {'self'}
876+
_exec_params = {k: v for k, v in self.optimizer_exec_params.items() if k in _exec_keys}
877+
self.optimizer.optimize(**_exec_params)
855878

856879
print('The optimization has been conducted.')
857880
self.search_conducted = True
@@ -875,9 +898,17 @@ def _create_optimizer(multiobjective_mode: bool, optimizer_init_params: dict,
875898
optimizer.pass_best_objectives(*best_sol_vals)
876899
else:
877900
optimizer_init_params['passed_population'] = population
878-
optimizer = SimpleOptimizer(**optimizer_init_params)
879-
880-
optimizer.set_strategy(opt_strategy_director)
901+
# Pass only the params SimpleOptimizer accepts. ``optimizer_init_params``
902+
# can still carry MOEA/D-only keys (e.g. ``H``, ``nds_method``) if
903+
# ``set_moeadd_params`` ran earlier -- which it does in EpdeSearch's
904+
# default (multiobjective) __init__ before a switch to single-objective.
905+
# Selecting by SimpleOptimizer's signature keeps this robust to how the
906+
# params dict was populated.
907+
so_keys = set(inspect.signature(SimpleOptimizer.__init__).parameters) - {'self'}
908+
so_params = {k: v for k, v in optimizer_init_params.items() if k in so_keys}
909+
optimizer = SimpleOptimizer(**so_params)
910+
911+
optimizer.set_strategy(opt_strategy_director)
881912
return optimizer
882913

883914
@property
@@ -1068,7 +1099,7 @@ def visualize_solutions(self, dimensions:list = [0, 1], **visulaizer_kwargs) ->
10681099
equations from the population. Furthermore, the annotate of the candidate equations are made with LaTeX toolkit.
10691100
'''
10701101
if self.multiobjective_mode:
1071-
self.optimizer.plot_pareto(dimensions=dimensions, **visulaizer_kwargs)
1102+
return self.optimizer.plot_pareto(dimensions=dimensions, **visulaizer_kwargs)
10721103
else:
10731104
raise NotImplementedError('Solution visualization is implemented only for multiobjective mode.')
10741105

@@ -1442,7 +1473,13 @@ def fit(self, samples: List[Tuple], equation_terms_max_number=6, equation_factor
14421473
else:
14431474
self.optimizer = optimizer
14441475

1445-
self.optimizer.optimize(**self.optimizer_exec_params)
1476+
# Pass only the exec params this optimizer's ``optimize`` accepts:
1477+
# SimpleOptimizer.optimize has no ``early_stopping_callback`` (a
1478+
# MOEA/D-only exec param that set_moeadd_params leaves behind when an
1479+
# EpdeSearch built multiobjective-by-default is run single-objective).
1480+
_exec_keys = set(inspect.signature(self.optimizer.optimize).parameters) - {'self'}
1481+
_exec_params = {k: v for k, v in self.optimizer_exec_params.items() if k in _exec_keys}
1482+
self.optimizer.optimize(**_exec_params)
14461483

14471484
print('The optimization has been conducted.')
14481485
self.search_conducted = True

epde/operators/common/coeff_calculation.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,8 @@ def _legacy_evaluate_nonzero(objective: Equation):
5454
"""Build target + un-normalised feature matrix from the LASSO
5555
survivors, independent of ``Equation.evaluate``.
5656
57-
Mirrors the pre-aaea0f4 legacy feature builder: iterate the
58-
structure, skip the target, emit columns only for terms whose
59-
``weights_internal`` slot is non-zero. Returns
57+
Iterate the structure, skip the target, and emit columns only for
58+
terms whose ``weights_internal`` slot is non-zero. Returns
6059
``(target, features)`` with ``features=None`` when every
6160
non-target slot was filtered to zero.
6261
"""

0 commit comments

Comments
 (0)