Skip to content

Commit 317b368

Browse files
authored
Merge pull request #60 from Gromwud/main
refactor and bugfixes
2 parents df057f5 + 0f09a1d commit 317b368

7 files changed

Lines changed: 87 additions & 89 deletions

File tree

epde/operators/common/fitness.py

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
116116
None
117117
"""
118118
self_args, subop_args = self.parse_suboperator_args(arguments=arguments)
119+
# self.suboperators['sparsity'].apply(objective, subop_args['sparsity'])
119120
if force_out_of_place:
120121
self.suboperators['sparsity'].apply(objective, subop_args['sparsity'])
121122
self.suboperators['coeff_calc'].apply(objective, subop_args['coeff_calc'])
@@ -126,29 +127,13 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
126127
data_shape = global_var.grid_cache.inner_shape
127128

128129
if features is None:
129-
# target_normalized = 2 * (target - target.min()) / (target.max() - target.min()) - 1
130-
# discr = target_normalized - target_normalized.mean()
131130
discr = target - target.mean()
132-
denom = target + target.mean()
133131
else:
134132
discr_feats = np.dot(features, objective.weights_final[:-1][objective.weights_internal != 0])
135133
discr_feats = discr_feats + objective.weights_final[-1]
136-
# discr = minmax_normalize(discr_feats.reshape(*data_shape)) - minmax_normalize(target.reshape(*data_shape))
137-
# discr = discr.flatten()
138-
# maximum = np.max((discr_feats.max(), target.max()))
139-
# minimum = np.min((discr_feats.min(), target.min()))
140-
# target_normalized = 2 * (target - minimum) / (maximum - minimum) - 1
141-
# discr = target_normalized - (2 * (discr_feats - minimum) / (maximum - minimum) - 1)
142134
discr = target - discr_feats
143-
denom = target + discr_feats
144135

145-
discr = np.multiply(discr, self.g_fun_vals)
146-
147-
# rl_error = np.mean(discr ** 2)
148-
# rl_error = np.sqrt(np.mean(discr ** 2)) / (target.max() - target.mean())
149-
# rl_error = np.sqrt(np.mean(discr ** 2)) / target.std()
150136
rl_error = np.sum(np.abs(discr)) / np.sum(np.abs(target))
151-
# rl_error = np.mean(np.abs(discr) / np.abs(denom)) / 2
152137

153138
if not (self.params['penalty_coeff'] > 0. and self.params['penalty_coeff'] < 1.):
154139
raise ValueError('Incorrect penalty coefficient set, value shall be in (0, 1).')
@@ -164,7 +149,6 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
164149
# Calculate r-loss
165150
target_vals = target.reshape(*data_shape)
166151
slices = [slice(None) for _ in range(target_vals.ndim)]
167-
# if not features is None:
168152
features_vals = features.reshape(*data_shape, -1)
169153
sample_weights_vals = self.g_fun_vals.reshape(*data_shape)
170154

@@ -178,46 +162,25 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool =
178162
else:
179163
step_size = num_horizons // horizons_default
180164
eq_window_weights = []
165+
181166
# Compute coefficients and collect statistics over horizons
182167
slices_window = slices.copy()
183-
# if features is None:
184-
# for start_idx in range(0, num_horizons, step_size):
185-
# end_idx = start_idx + window_size
186-
# slices_window[dim] = slice(start_idx, end_idx)
187-
# target_window = target_vals[*slices_window]
188-
# eq_window_weights.append(target_window.mean())
189-
# lr += np.sqrt(np.std(eq_window_weights, ddof=1) ** 2 / (np.std(eq_window_weights, ddof=1) ** 2 + np.mean(eq_window_weights) ** 2))
190-
# else:
191168
for start_idx in range(0, num_horizons, step_size):
192169
end_idx = start_idx + window_size
193170
slices_window[dim] = slice(start_idx, end_idx)
194171
target_window = target_vals[*slices_window].reshape(-1)
195172
feature_window = features_vals[*slices_window, :].reshape(-1, features.shape[-1])
196173
sample_weights_window = sample_weights_vals[*slices_window].reshape(-1)
197174
estimator = LinearRegression(fit_intercept=True)
198-
# estimator = Ridge(alpha=0, copy_X=True, fit_intercept=True, max_iter=20,
199-
# positive=False, random_state=None, tol=0.0001, solver='sparse_cg')
200175
estimator.fit(feature_window, target_window, sample_weight=sample_weights_window)
201176
valuable_weights = estimator.coef_
202177
eq_window_weights.append(valuable_weights)
203178
std = np.array(eq_window_weights).std(axis=0, ddof=1)
204179
mu = np.array(eq_window_weights).mean(axis=0)
205-
# scale = []
206-
# for feature in range(features.shape[-1]):
207-
# # scale.append(feature_window[:, feature] ** 2 / (feature_window[:, feature] ** 2 + target_window ** 2))
208-
# scale.append(np.linalg.norm(feature_window[:, feature] * mu[feature], ord=2) / np.linalg.norm(target_window, ord=2))
209-
# scale = np.array(scale) / sum(scale)
210-
# eq_cv = np.sqrt(std ** 2 / (std ** 2 + mu ** 2) * scale)
211-
# eq_cv = np.sqrt(std ** 2 / (std ** 2 + mu ** 2)) * scale
212180
eq_cv = std ** 2 / (mu ** 2)
213181
lr += np.nan_to_num(eq_cv).sum()
214182

215183
lr = lr / (len(objective.structure) - 1) / target_vals.ndim
216-
# if force_out_of_place:
217-
# return lr
218-
219-
fv = 1 - np.abs(np.log10(fitness_value + 1e-9) / 8)
220-
lrt = 1 - np.abs(np.log10(lr + 1e-9) / 8)
221184

222185
objective.fitness_calculated = True
223186
objective.fitness_value = fitness_value

epde/operators/common/right_part_selection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ def apply(self, objective : Equation, arguments : dict):
6161
continue
6262
objective.target_idx = target_idx
6363
fitness = self.suboperators['fitness_calculation'].apply(objective, arguments = subop_args['fitness_calculation'], force_out_of_place = True)
64-
if fitness < min_fitness:
64+
if fitness < min_fitness and not all(objective.weights_internal == 0):
6565
min_fitness = fitness
6666
min_idx = target_idx
6767
weights_internal = objective.weights_internal
6868
else:
6969
pass
7070

71-
if all(weights_internal == 0):
71+
if all(weights_internal == 0) or np.isinf(min_fitness):
7272
objective.randomize()
7373
continue
7474

epde/operators/common/sparsity.py

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717
import time
1818
from sklearn.base import BaseEstimator, RegressorMixin
1919
from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
20+
import seaborn as sns
21+
import matplotlib.pyplot as plt
2022

2123

2224
class CustomPhysicsLasso(BaseEstimator, RegressorMixin):
23-
def __init__(self, max_iter=100, tol=1e-4):
25+
def __init__(self, max_iter=20, tol=1e-4):
2426
self.max_iter = max_iter
2527
self.tol = tol
2628

@@ -33,6 +35,7 @@ def get_cv(self, weights):
3335
# cv = std ** 2 / (std ** 2 + mu ** 2)
3436
# cv = np.sqrt(std ** 2 / (std ** 2 + mu ** 2))
3537
cv = std ** 2 / (mu ** 2)
38+
# cv = abs(std / mu)
3639
return cv
3740

3841
def calculate_weights(self, X, y):
@@ -45,12 +48,13 @@ def calculate_weights(self, X, y):
4548
w_full, _, _, _ = np.linalg.lstsq(X_batch, y_batch, rcond=None)
4649
weights.append(w_full)
4750

48-
return weights
51+
return np.array(weights)
4952

5053
def fit(self, X, y):
5154
X, y = check_X_y(X, y, dtype=np.float64)
5255
self.n_samples, self.n_features = X.shape
5356
self.batch_size = int(self.n_samples * 0.5) # 50% of data
57+
# self.batch_size = self.n_features + 1
5458

5559
# --- 1. Initialization ---
5660
# Add column of 1s to solve for intercept correctly via OLS
@@ -60,6 +64,25 @@ def fit(self, X, y):
6064
self.coef_ = np.array(weights).mean(axis=0)[:-1]
6165
self.intercept_ = np.array(weights).mean(axis=0)[-1]
6266

67+
# # Create the figure and axes
68+
# fig, axs = plt.subplots(2, 1, figsize=(8, 6))
69+
#
70+
# # Subplot 1: Coefficients
71+
# sns.barplot(x=['d^2u/dx^2', "u^3", "u", "du/dx * d^2u/dx^2"], y=self.coef_, ax=axs[0], color='tab:blue')
72+
# axs[0].set_yscale("symlog", linthresh=1e-8)
73+
# axs[0].set_title("Coefficients")
74+
# axs[0].set_ylabel("Coefficient Value")
75+
#
76+
# # Subplot 2: CV (excluding last element)
77+
# # sns.barplot(x=np.arange(len(cv) - 1), y=cv[:-1], ax=axs[1], color='tab:red')
78+
# sns.barplot(x=['d^2u/dx^2', "u^3", "u", "du/dx * d^2u/dx^2"], y=cv[:-1], ax=axs[1], color='tab:red')
79+
# axs[1].set_yscale("log")
80+
# axs[1].set_title("Instability of Coefficients")
81+
# axs[1].set_ylabel("Value (Log)")
82+
#
83+
# plt.tight_layout()
84+
# plt.show()
85+
6386
# Pre-compute norms of features (optimization)
6487
# These are constant throughout the loop
6588
norm_sq_features = np.sum(X ** 2, axis=0)
@@ -68,11 +91,9 @@ def fit(self, X, y):
6891
y_pred = X @ self.coef_ + self.intercept_
6992
residual = y - y_pred
7093

71-
max_change_old = 0.0
72-
7394
# --- 2. Coordinate Descent Loop ---
74-
for iteration in range(self.max_iter):
75-
max_change = 0.0
95+
for iteration in range(self.max_iter * self.n_features):
96+
max_change = self.tol
7697

7798
# A. Update Intercept (Unpenalized)
7899
# The optimal intercept shift is simply the mean of the residuals
@@ -82,17 +103,13 @@ def fit(self, X, y):
82103
residual -= intercept_shift
83104

84105
# B. Update Coefficients
85-
for j in range(self.n_features):
106+
for j in np.argsort(cv[:-1])[::-1]:
86107
if self.coef_[j] == 0:
87108
continue
88109

89110
old_coef = self.coef_[j]
90111
norm_sq = norm_sq_features[j]
91112

92-
# Skip constant columns to avoid division by zero
93-
# if norm_sq == 0:
94-
# continue
95-
96113
# 1. Calculate partial residual correlation
97114
# This represents the correlation between feature j and the target
98115
# if feature j were removed from the model.
@@ -102,19 +119,57 @@ def fit(self, X, y):
102119
# 2. Soft Thresholding
103120
# Threshold is N * alpha
104121
threshold = cv[j] * sum(y ** 2)
122+
# threshold = cv[j] * norm_sq
123+
# threshold = cv[j]
105124
new_coef = self._soft_threshold(rho, threshold) / norm_sq
106125

107126
# 3. Update State
108127
self.coef_[j] = new_coef
128+
if new_coef == 0:
129+
weights = self.calculate_weights(X[:, self.coef_ != 0], y)
130+
new_cv = self.get_cv(weights)
131+
mask = self.coef_ != 0
132+
mask = np.append(mask, True)
133+
iter_cv = iter(new_cv)
134+
cv = [next(iter_cv) if val else 0 for val in mask]
135+
136+
new_coefs = np.array(weights).mean(axis=0)[:-1]
137+
iter_coefs = iter(new_coefs)
138+
self.coef_ = np.array([next(iter_coefs) if val else 0 for val in mask[:-1]])
139+
self.intercept_ = np.array(weights).mean(axis=0)[-1]
140+
141+
y_pred = X @ self.coef_ + self.intercept_
142+
residual = y - y_pred
143+
144+
# # Create the figure and axes
145+
# fig, axs = plt.subplots(2, 1, figsize=(8, 6))
146+
#
147+
# # Subplot 1: Coefficients
148+
# # sns.barplot(x=np.arange(len(self.coef_)), y=self.coef_, ax=axs[0], color='tab:blue')
149+
# sns.barplot(x=['d^2u/dx^2', "u^3", "u", "du/dx * d^2u/dx^2"], y=self.coef_, ax=axs[0], color='tab:blue')
150+
# axs[0].set_yscale("symlog", linthresh=1e-8)
151+
# axs[0].set_title("Coefficients")
152+
# axs[0].set_ylabel("Coefficient Value")
153+
#
154+
# # Subplot 2: CV (excluding last element)
155+
# # sns.barplot(x=np.arange(len(cv) - 1), y=cv[:-1], ax=axs[1], color='tab:red')
156+
# sns.barplot(x=['d^2u/dx^2', "u^3", "u", "du/dx * d^2u/dx^2"], y=cv[:-1], ax=axs[1], color='tab:red')
157+
# axs[1].set_yscale("log")
158+
# axs[1].set_title("Instability of Coefficients")
159+
# axs[1].set_ylabel("Value (Log)")
160+
#
161+
# plt.tight_layout()
162+
# plt.show()
163+
break
164+
109165
# Update residual vector efficiently
110166
# r_new = r_old - (w_new - w_old) * X_j
111167
residual -= (new_coef - old_coef) * X[:, j]
112168
max_change = max(max_change, abs((new_coef - old_coef) / old_coef))
169+
else:
170+
if max_change < self.tol:
171+
break
113172

114-
if abs(max_change - max_change_old) < self.tol:
115-
break
116-
117-
max_change_old = max_change
118173

119174
self.n_iter_ = iteration + 1
120175
# print("-------")

epde/operators/multiobjective/moeadd_specific.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,9 @@ def apply(self, objective: ParetoLevels, arguments: dict):
434434
while objective.unplaced_candidates:
435435
offspring = objective.unplaced_candidates.pop()
436436
attempt = 0
437-
# replaced = 0
437+
replaced = 0
438438
mutation_attempt_limit = self.params['mutation_attempt_limit']
439-
# offspring_attempt_limit = self.params['offspring_attempt_limit']
439+
offspring_attempt_limit = self.params['offspring_attempt_limit']
440440
# self.suboperators['sparsity'].apply(objective=offspring,
441441
# arguments=subop_args['sparsity'])
442442
temp_offspring = deepcopy(offspring)
@@ -464,13 +464,13 @@ def apply(self, objective: ParetoLevels, arguments: dict):
464464
objective.history.add(system)
465465
print(temp_offspring.obj_fun)
466466
break
467-
elif attempt == mutation_attempt_limit:
467+
elif attempt == offspring_attempt_limit:
468468
print("Could not generate unique offspring")
469469
break
470-
# elif attempt == mutation_attempt_limit:
471-
# temp_offspring = deepcopy(offspring)
472-
# replaced += 1
473-
# attempt = 0
470+
elif attempt == mutation_attempt_limit:
471+
temp_offspring.create()
472+
replaced += 1
473+
attempt = 0
474474
attempt += 1
475475
return objective
476476

epde/operators/multiobjective/mutations.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best
3434
# altered_eq = self.suboperators['equation_mutation'].apply(altered_objective.vals[eq_key],
3535
# subop_args['equation_mutation'])
3636
for eq_key in eqs_keys:
37-
affected_by_mutation = np.random.random() < (self.params['indiv_mutation_prob'] / len(eqs_keys))
37+
affected_by_mutation = np.random.random() < self.params['indiv_mutation_prob']
3838
if affected_by_mutation:
3939
altered_eq = self.suboperators['equation_mutation'].apply(altered_objective.vals[eq_key],
4040
subop_args['equation_mutation'])
@@ -59,19 +59,6 @@ class EquationMutation(CompoundOperator):
5959
def apply(self, objective : Equation, arguments : dict):
6060
self_args, subop_args = self.parse_suboperator_args(arguments = arguments)
6161

62-
# for term_idx in range(objective.n_immutable, len(objective.structure)):
63-
# if np.random.uniform(0, 1) <= self.params['r_mutation']:
64-
# objective.structure[term_idx] = self.suboperators['mutation'].apply(objective = (term_idx, objective),
65-
# arguments = subop_args['mutation'])
66-
# nonzero_terms_mask = np.array([False if weight == 0 else True for weight in objective.weights_internal],
67-
# dtype=np.integer)
68-
# nonrs_terms_idx = [i for i, term in enumerate(objective.structure) if i != objective.target_idx]
69-
# nonzero_terms_idx = [item for item, keep in zip(nonrs_terms_idx, nonzero_terms_mask) if keep]
70-
# nonzero_terms_idx.append(objective.target_idx)
71-
# if len(nonzero_terms_idx) > 0:
72-
# term_idx = np.random.choice(nonzero_terms_idx)
73-
# else:
74-
# term_idx = objective.target_idx
7562
term_idx = np.random.choice(range(len(objective.structure)))
7663
objective.structure[term_idx] = self.suboperators['mutation'].apply(objective=(term_idx, objective),
7764
arguments=subop_args['mutation'])
@@ -91,16 +78,7 @@ def apply(self, objective : Union[int, float], arguments : dict):
9178
altered_objective = np.random.normal(objective, objective)
9279
if altered_objective < 0:
9380
altered_objective = - altered_objective
94-
# if altered_objective > 1:
95-
# altered_objective = 1
96-
97-
# altered_objective = objective + np.random.randint(-1, 2)
98-
# if altered_objective < 1:
99-
# altered_objective = 1
100-
# if altered_objective > 4:
101-
# altered_objective = 4
102-
#
103-
# return altered_objective
81+
10482
return np.float64(altered_objective)
10583

10684
def use_default_tags(self):

epde/operators/utils/parameters/default_parameters_multi_objective.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"term_param_proportion" : 0.4
5353
},
5454
"SystemMutation" : {
55-
"indiv_mutation_prob" : 1
55+
"indiv_mutation_prob" : 0.6
5656
},
5757
"EquationMutation" : {
5858
"r_mutation" : 0.6

epde/optimizers/moeadd/moeadd.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ def marriageSolutionAssignment(weights: np.ndarray, solutions: List[MOEADDSoluti
6363
assert len(solutions) == weights.shape[0], f'Solutions do not match weights in length: {len(solutions)} vs {weights.shape[0]}.'
6464

6565
acute_angles = np.empty((weights.shape[0], weights.shape[0]))
66+
6667
for i, weight in enumerate(weights):
68+
weight_full = [item for item in weight for _ in solutions[0].vals]
6769
for j, solution in enumerate(solutions):
68-
acute_angles[i, j] = acute_angle(weight, solution.obj_fun)
70+
acute_angles[i, j] = acute_angle(weight_full, solution.obj_fun)
6971

7072
w_preferences = np.argsort(acute_angles, axis = 1)
7173

0 commit comments

Comments
 (0)