1313import matplotlib .pyplot as plt
1414from 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).
1720from epde .structure .main_structures import SoEq , Equation
1821from epde .operators .utils .template import CompoundOperator
1922import 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