Skip to content

Commit 3f5e5e8

Browse files
committed
Objective normalizations & minor tweaks
1 parent 62e4266 commit 3f5e5e8

7 files changed

Lines changed: 81 additions & 25 deletions

File tree

epde/interface/prepared_tokens.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,21 +222,23 @@ class TrigonometricTokens(PreparedTokens):
222222
"""
223223
Class for prepared tokens, that belongs to the trigonometric family
224224
"""
225-
def __init__(self, freq: tuple = (np.pi/2., 2*np.pi), dimensionality=1):
225+
def __init__(self, freq: tuple = (np.pi/2., 2*np.pi), dimensionality: int = 1, meaningful: bool = False):
226226
"""
227227
Initialization of class
228228
229229
Args:
230230
freq (`tuple`): optional, default - (pi/2., 2*pi)
231-
interval for parameter frequency in trigonometric token
231+
interval for parameter frequency in trigonometric token.
232232
dimensionality (`int`): optional, default - 1
233-
data dimension
233+
data dimension.
234+
meaningful (`bool`): optional, defaule - False
235+
flag, if the token can be used as a separate term or term-forming factor in the equation.
234236
"""
235237
assert freq[1] > freq[0] and len(freq) == 2, 'The tuple, defining frequncy interval, shall contain 2 elements with first - the left boundary of interval and the second - the right one. '
236238

237239
self._token_family = TokenFamily(token_type='trigonometric')
238240
self._token_family.set_status(unique_specific_token=True, unique_token_type=True,
239-
meaningful=False)
241+
meaningful=meaningful)
240242

241243
def latex_form(label, **params):
242244
'''

epde/interface/token_family.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def chech_constancy(self, **tfkwargs):
275275
try:
276276
constancy = np.isclose(np.min(data), np.max(data))
277277
except TypeError:
278-
print(f"No {label} data in cache!")
278+
print(f"No {label} data in cache for constancy check. Functionality of eval-d token check TBD.")
279279
continue
280280
if constancy:
281281
constant_tokens_labels.append(label)

epde/operators/common/right_part_selection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def simplify_equation(self, objective: Equation):
115115
term.structure = [factor for factor in term.structure if factor not in factors_simplified]
116116
term.reset_saved_state()
117117
# If term's order became zero -- replace term
118-
if len(term.structure) == 0:
118+
if (len(term.structure) == 0 or not term.contains_meaningful()):
119119
term.randomize()
120120
term.reset_saved_state()
121121
while objective.structure.count(term) > 1 or term == temp:

epde/operators/multiobjective/moeadd_specific.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,18 @@
88
import copy
99
import numpy as np
1010
import time
11-
from typing import Union
11+
from typing import Union, Tuple
1212
from functools import reduce, partial
1313

14-
from epde.optimizers.moeadd.moeadd import ParetoLevels
14+
from epde.optimizers.moeadd.moeadd import ParetoLevels, ObjFunNormalizer
1515
from epde.operators.utils.template import CompoundOperator, add_base_param_to_operator
1616
from epde.operators.multiobjective.mutations import get_basic_mutation
1717

18+
from epde.structure.main_structures import SoEq
1819

19-
def penalty_based_intersection(sol_obj, weight, ideal_obj, penalty_factor = 1.) -> float:
20+
21+
def penalty_based_intersection(sol_obj, weight, ideal_obj,
22+
penalty_factor = 1., obj_normalizer: ObjFunNormalizer = None) -> float:
2023
'''
2124
Calculation of the penalty pased intersection, that is minimized for the solutions inside the
2225
domain, specified by **weight** vector. The calculations are held, according to the following formulas:
@@ -50,10 +53,17 @@ def penalty_based_intersection(sol_obj, weight, ideal_obj, penalty_factor = 1.)
5053
5154
penalty_factor : float, optional, default 1.
5255
The penalty parameter, represents :math:`\Theta` in the equations.
56+
57+
obj_normalizer : ObjFunNormalizer obj., optional, defaut None.
58+
Normalizer for solution objective functions.
5359
5460
'''
55-
d_1 = np.dot((sol_obj.obj_fun - ideal_obj), weight) / np.linalg.norm(weight)
56-
d_2 = np.linalg.norm(sol_obj.obj_fun - (ideal_obj + d_1 * weight/np.linalg.norm(weight)))
61+
print(f'Objective before normalization: {sol_obj.obj_fun} for normalizer {obj_normalizer}')
62+
solution_objective = sol_obj.obj_fun if obj_normalizer is None else obj_normalizer(sol_obj.obj_fun)
63+
print(f'Objective after expected normalization: {solution_objective}')
64+
65+
d_1 = np.dot((solution_objective - ideal_obj), weight) / np.linalg.norm(weight)
66+
d_2 = np.linalg.norm(solution_objective - (ideal_obj + d_1 * weight/np.linalg.norm(weight)))
5767
return d_1 + penalty_factor * d_2
5868

5969

@@ -87,7 +97,7 @@ def population_to_sectors(population, weights):
8797
return list(map(solution_selection, np.arange(len(weights))))
8898

8999

90-
def locate_pareto_worst(levels, weights, best_obj, penalty_factor = 1.):
100+
def locate_pareto_worst(levels: ParetoLevels, weights: np.ndarray, best_obj: np.ndarray, penalty_factor: float = 1.):
91101
'''
92102
93103
Function, dedicated to the selection of the worst solution on the Pareto levels.
@@ -114,7 +124,8 @@ def locate_pareto_worst(levels, weights, best_obj, penalty_factor = 1.):
114124
if len(crowded_domains) == 1:
115125
most_crowded_domain = crowded_domains[0]
116126
else:
117-
PBI = lambda domain_idx: sum([penalty_based_intersection(sol_obj, weights[domain_idx], best_obj, penalty_factor) for sol_obj in domain_solutions[domain_idx]])
127+
PBI = lambda domain_idx: sum([penalty_based_intersection(sol_obj, weights[domain_idx], best_obj, penalty_factor, levels.normalizer)
128+
for sol_obj in domain_solutions[domain_idx]])
118129
PBIS = np.fromiter(map(PBI, crowded_domains), dtype = float)
119130
most_crowded_domain = crowded_domains[np.argmax(PBIS)]
120131

@@ -127,21 +138,34 @@ def locate_pareto_worst(levels, weights, best_obj, penalty_factor = 1.):
127138
max_level = np.max(domain_solution_NDL_idxs)
128139
worst_NDL_section = [domain_solutions[most_crowded_domain][sol_idx] for sol_idx in np.arange(len(domain_solutions[most_crowded_domain]))
129140
if domain_solution_NDL_idxs[sol_idx] == max_level]
130-
PBIS = np.fromiter(map(lambda solution: penalty_based_intersection(solution, weights[most_crowded_domain], best_obj, penalty_factor), worst_NDL_section), dtype = float)
141+
PBIS = np.fromiter(map(lambda solution: penalty_based_intersection(solution, weights[most_crowded_domain], best_obj, penalty_factor, levels.normalizer),
142+
worst_NDL_section), dtype = float)
131143
return worst_NDL_section[np.argmax(PBIS)]
132144

133145

134146
class PopulationUpdater(CompoundOperator):
135147
key = 'PopulationUpdater'
136148

137-
def apply(self, objective : ParetoLevels, arguments : dict):
149+
def apply(self, objective : Tuple[Union[SoEq, ParetoLevels]], arguments : dict):
138150
'''
139151
Update population to get the pareto-nondomiated levels with the worst element removed.
140152
Here, "worst" means the solution with highest PBI value (penalty-based boundary intersection)
141-
'''
153+
'''
154+
assert isinstance(objective, tuple), f'Expected input of PopulationUpdater to be a Tuple of SoEq and ParetoLevels.\n'\
155+
f'Did not get even a Tuple, instead got {type(objective)}!'
156+
assert isinstance(objective[0], SoEq), f'Expected input of PopulationUpdater to be a Tuple of SoEq and ParetoLevels.\n'\
157+
f'Did not get a SoEq obj in the first position, instead got {type(objective[0])}!'
158+
assert isinstance(objective[1], ParetoLevels), f'Expected input of PopulationUpdater to be a Tuple of SoEq and ParetoLevels.\n'\
159+
f'Did not get even a ParetoLevels in the second position, '\
160+
f'instead got {type(objective[1])}!.'
161+
142162
self_args, subop_args = self.parse_suboperator_args(arguments = arguments)
143163
# print(f'PopulationUpdater.params is {self.params}')
144164

165+
# TODO: Init normalizer here!
166+
# print('objective is ', objective)
167+
# objective[1].set_normalizer()
168+
145169
objective[1].update(objective[0]) #levels_updated = ndl_update(offspring, levels)
146170
if len(objective[1].levels) == 1:
147171
worst_solution = locate_pareto_worst(objective[1], self_args['weights'],
@@ -168,7 +192,8 @@ def apply(self, objective : ParetoLevels, arguments : dict):
168192
else:
169193
PBI = lambda domain_idx: np.sum([penalty_based_intersection(sol_obj, self_args['weights'][domain_idx],
170194
self_args['best_obj'],
171-
self.params['PBI_penalty'])
195+
self.params['PBI_penalty'],
196+
objective[1].normalizer)
172197
for sol_obj in last_level_by_domains[domain_idx]])
173198
PBIS = np.fromiter(map(PBI, crowded_domains), dtype = float)
174199
most_crowded_domain = crowded_domains[np.argmax(PBIS)]
@@ -179,7 +204,8 @@ def apply(self, objective : ParetoLevels, arguments : dict):
179204
else:
180205
PBIS = np.fromiter(map(lambda solution: penalty_based_intersection(solution,
181206
self_args['weights'][most_crowded_domain],
182-
self_args['best_obj'], self.params['PBI_penalty']),
207+
self_args['best_obj'], self.params['PBI_penalty'],
208+
objective[1].normalizer),
183209
last_level_by_domains[most_crowded_domain]), dtype = float)
184210
worst_solution = last_level_by_domains[most_crowded_domain][np.argmax(PBIS)]
185211

@@ -242,7 +268,8 @@ def apply(self, objective : ParetoLevels, arguments : dict):
242268
most_crowded_domain = crowded_domains[0]
243269
else:
244270
PBI = lambda domain_idx: np.sum([penalty_based_intersection(sol_obj, self_args['weights'][domain_idx],
245-
self_args['best_obj'], self.params['PBI_penalty'])
271+
self_args['best_obj'], self.params['PBI_penalty'],
272+
objective.normalizer)
246273
for sol_obj in last_level_by_domains[domain_idx]])
247274
PBIS = np.fromiter(map(PBI, crowded_domains), dtype = float)
248275
most_crowded_domain = crowded_domains[np.argmax(PBIS)]
@@ -417,7 +444,7 @@ def apply(self, objective : ParetoLevels, arguments : dict):
417444
418445
'''
419446
self_args, subop_args = self.parse_suboperator_args(arguments = arguments)
420-
447+
421448
if len(objective.population) == 0:
422449
for idx, candidate in enumerate(objective.unplaced_candidates):
423450
self.suboperators['right_part_selector'].apply(objective = candidate,
@@ -432,6 +459,10 @@ def apply(self, objective : ParetoLevels, arguments : dict):
432459
arguments=subop_args['chromosome_fitness'])
433460
objective.history.add(tuple(candidate.obj_fun))
434461
objective.initial_placing()
462+
463+
# TODO: consider carefully, where normalizer init shall be held. If here, only the initial values are employed
464+
objective.set_normalizer()
465+
435466
return objective
436467

437468
def get_initial_sorter(right_part_selector : CompoundOperator,

epde/operators/multiobjective/mutations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def apply(self, objective : tuple, arguments : dict): #term_idx, equation):
115115
new_term = Term(objective[1].pool, mandatory_family = objective[1].structure[objective[0]].descr_variable_marker,
116116
create_derivs=create_derivs,
117117
max_factors_in_term = objective[1].metaparameters['max_factors_in_term']['value'])
118-
while not check_uniqueness(new_term, objective[1].structure[:objective[0]] + objective[1].structure[objective[0]+1:]):
118+
while not (check_uniqueness(new_term, objective[1].structure[:objective[0]] + objective[1].structure[objective[0]+1:]) and
119+
new_term.contains_meaningful()):
119120
new_term = Term(objective[1].pool, mandatory_family = objective[1].structure[objective[0]].descr_variable_marker,
120121
create_derivs=create_derivs,
121122
max_factors_in_term = objective[1].metaparameters['max_factors_in_term']['value'])

epde/optimizers/moeadd/moeadd.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from itertools import chain
1010

1111
from typing import Union, List
12+
from functools import reduce
1213

1314
def flatten_chain(matrix):
1415
return list(chain.from_iterable(matrix))
@@ -32,6 +33,15 @@ def clear_list_of_lists(inp_list) -> list:
3233
return [elem for elem in inp_list if len(elem) > 0]
3334

3435

36+
class ObjFunNormalizer(object):
37+
def __init__(self, obj_worst_vals: np.ndarray):
38+
self._worst_vals = obj_worst_vals
39+
40+
def __call__(self, obj_vals: np.ndarray):
41+
assert obj_vals.size == self._worst_vals.size, 'Passed objective values have different length, than stored max ones.'
42+
return obj_vals / self._worst_vals
43+
44+
3545
class ParetoLevels(object):
3646
'''
3747
@@ -53,7 +63,7 @@ class ParetoLevels(object):
5363
5464
'''
5565
def __init__(self, population, sorting_method = fast_non_dominated_sorting,
56-
update_method = ndl_update, initial_sort = False):
66+
update_method = ndl_update): # , initial_sort = False
5767
"""
5868
Args:
5969
population (`list`): List with the elements - canidate solutions of the case-specific subclass of
@@ -67,6 +77,8 @@ def __init__(self, population, sorting_method = fast_non_dominated_sorting,
6777
self.population = []
6878
self._update_method = update_method
6979
self.unplaced_candidates = population
80+
81+
self.normalizer = None
7082
self.history = set()
7183

7284
def manual_reconst(self, attribute:str, value, except_attrs:dict):
@@ -87,6 +99,15 @@ def attrs_from_dict(self, attributes, except_keys = ['obj_type']):
8799
self.__dict__ = {key : item for key, item in attributes.items()
88100
if key not in except_keys}
89101

102+
def set_normalizer(self):
103+
# worst_objectives = reduce(lambda x, y: x.extend(y),
104+
# [[elem.obj_fun for elem in level] for level in self.levels], []) # : np.ndarray
105+
objectives = np.stack(reduce(lambda x, y: x.extend(y) or x,
106+
[[elem.obj_fun for elem in level] for level in self.levels]), axis = 0)
107+
108+
109+
self.normalizer = ObjFunNormalizer(np.max(objectives, axis = 0))
110+
90111
@property
91112
def levels(self):
92113
return self._levels
@@ -338,8 +359,7 @@ def __init__(self, population_instruct, weights_num, pop_size, solution_params,
338359
Confirmed {len(population)}/{pop_size} solutions.')
339360
break
340361
solution_gen_idx += 1
341-
self.pareto_levels = ParetoLevels(population, sorting_method = nds_method, update_method = ndl_update,
342-
initial_sort = False)
362+
self.pareto_levels = ParetoLevels(population, sorting_method = nds_method, update_method = ndl_update) # initial_sort = False
343363
else:
344364
if not isinstance(passed_population, ParetoLevels):
345365
raise TypeError(f'Incorrect type of the population passed. Expected ParetoLevels object, instead got \
@@ -449,7 +469,6 @@ def set_strategy(self, strategy_director):
449469
builder.assemble(True)
450470
self.set_sector_processer(builder.processer)
451471

452-
453472
def optimize(self, epochs):
454473
"""
455474
Method for the main unconstrained evolutionary optimization. Can be applied repeatedly to

epde/structure/main_structures.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ def contains_deriv(self, variable=None):
327327
def contains_variable(self, variable):
328328
return any([factor.variable == variable for factor in self.structure])
329329

330+
def contains_meaningful(self):
331+
return any([factor.status['meaningful'] for factor in self.structure])
332+
330333
def __eq__(self, other):
331334
return (all([any([other_elem == self_elem for other_elem in other.structure]) for self_elem in self.structure])
332335
and all([any([other_elem == self_elem for self_elem in self.structure]) for other_elem in other.structure])

0 commit comments

Comments
 (0)