SKSurrogate 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- SKSurrogate/DataProcess.py +409 -0
- SKSurrogate/NpyProximation.py +457 -0
- SKSurrogate/__init__.py +110 -0
- SKSurrogate/aml.py +1156 -0
- SKSurrogate/ci.py +93 -0
- SKSurrogate/deployment.py +31 -0
- SKSurrogate/eoa.py +381 -0
- SKSurrogate/execution.py +416 -0
- SKSurrogate/inference.py +224 -0
- SKSurrogate/mltrace.py +2138 -0
- SKSurrogate/modelbundle.py +600 -0
- SKSurrogate/monitoring.py +360 -0
- SKSurrogate/retraining.py +49 -0
- SKSurrogate/sensapprx.py +250 -0
- SKSurrogate/structsearch.py +1411 -0
- SKSurrogate/synthdat.py +503 -0
- sksurrogate-0.1.0.dist-info/METADATA +139 -0
- sksurrogate-0.1.0.dist-info/RECORD +21 -0
- sksurrogate-0.1.0.dist-info/WHEEL +5 -0
- sksurrogate-0.1.0.dist-info/entry_points.txt +2 -0
- sksurrogate-0.1.0.dist-info/top_level.txt +1 -0
SKSurrogate/ci.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Quality gates for validating model bundles in CI pipelines."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from numbers import Real
|
|
5
|
+
|
|
6
|
+
from .modelbundle import ModelBundle, load_bundle
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BundleQualityGateError(ValueError):
|
|
10
|
+
"""Raised when a model bundle fails one or more CI quality gates."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_bundle(bundle_or_path, expected_schema):
|
|
14
|
+
if isinstance(bundle_or_path, ModelBundle):
|
|
15
|
+
if expected_schema is not None and bundle_or_path.schema != expected_schema:
|
|
16
|
+
raise ValueError("Model bundle schema is incompatible with the expected schema")
|
|
17
|
+
return bundle_or_path, None
|
|
18
|
+
path = Path(bundle_or_path)
|
|
19
|
+
return load_bundle(path, expected_schema=expected_schema, strict_dependencies=False), path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def check_bundle_quality(
|
|
23
|
+
bundle_or_path,
|
|
24
|
+
*,
|
|
25
|
+
expected_schema=None,
|
|
26
|
+
metric_thresholds=None,
|
|
27
|
+
max_bundle_size_bytes=None,
|
|
28
|
+
):
|
|
29
|
+
"""Return structured CI checks for a model bundle.
|
|
30
|
+
|
|
31
|
+
``metric_thresholds`` maps metric names to minimum accepted numeric values.
|
|
32
|
+
A bundle path is required when ``max_bundle_size_bytes`` is configured.
|
|
33
|
+
"""
|
|
34
|
+
checks = []
|
|
35
|
+
failures = []
|
|
36
|
+
try:
|
|
37
|
+
bundle, path = _load_bundle(bundle_or_path, expected_schema)
|
|
38
|
+
checks.append({"name": "schema_compatibility", "passed": True})
|
|
39
|
+
except (OSError, TypeError, ValueError) as exc:
|
|
40
|
+
checks.append({"name": "schema_compatibility", "passed": False, "error": str(exc)})
|
|
41
|
+
failures.append(checks[-1])
|
|
42
|
+
return {"passed": False, "checks": checks, "failures": failures}
|
|
43
|
+
|
|
44
|
+
for metric_name, minimum in (metric_thresholds or {}).items():
|
|
45
|
+
value = bundle.metrics.get(metric_name)
|
|
46
|
+
passed = isinstance(value, Real) and not isinstance(value, bool) and value >= minimum
|
|
47
|
+
check = {
|
|
48
|
+
"name": "metric_threshold",
|
|
49
|
+
"metric": metric_name,
|
|
50
|
+
"minimum": minimum,
|
|
51
|
+
"value": value,
|
|
52
|
+
"passed": passed,
|
|
53
|
+
}
|
|
54
|
+
checks.append(check)
|
|
55
|
+
if not passed:
|
|
56
|
+
failures.append(check)
|
|
57
|
+
|
|
58
|
+
if max_bundle_size_bytes is not None:
|
|
59
|
+
size = path.stat().st_size if path is not None else None
|
|
60
|
+
passed = size is not None and size <= max_bundle_size_bytes
|
|
61
|
+
check = {
|
|
62
|
+
"name": "bundle_size",
|
|
63
|
+
"maximum": max_bundle_size_bytes,
|
|
64
|
+
"value": size,
|
|
65
|
+
"passed": passed,
|
|
66
|
+
}
|
|
67
|
+
checks.append(check)
|
|
68
|
+
if not passed:
|
|
69
|
+
failures.append(check)
|
|
70
|
+
|
|
71
|
+
return {"passed": not failures, "checks": checks, "failures": failures}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def assert_bundle_quality(
|
|
75
|
+
bundle_or_path,
|
|
76
|
+
*,
|
|
77
|
+
expected_schema=None,
|
|
78
|
+
metric_thresholds=None,
|
|
79
|
+
max_bundle_size_bytes=None,
|
|
80
|
+
):
|
|
81
|
+
"""Raise :class:`BundleQualityGateError` when CI checks fail."""
|
|
82
|
+
report = check_bundle_quality(
|
|
83
|
+
bundle_or_path,
|
|
84
|
+
expected_schema=expected_schema,
|
|
85
|
+
metric_thresholds=metric_thresholds,
|
|
86
|
+
max_bundle_size_bytes=max_bundle_size_bytes,
|
|
87
|
+
)
|
|
88
|
+
if not report["passed"]:
|
|
89
|
+
raise BundleQualityGateError("Model bundle failed quality gates: %s" % report["failures"])
|
|
90
|
+
return report
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["BundleQualityGateError", "assert_bundle_quality", "check_bundle_quality"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Approval-gated model promotion and rollback helpers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DeploymentApprovalGate:
|
|
5
|
+
"""Require approvals and passing quality checks before promotion."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, registry, required_approvals=1):
|
|
8
|
+
if required_approvals < 1:
|
|
9
|
+
raise ValueError("required_approvals must be at least 1")
|
|
10
|
+
self.registry = registry
|
|
11
|
+
self.required_approvals = required_approvals
|
|
12
|
+
|
|
13
|
+
def promote(self, task_name, model_version, state, *, approvers, quality_report=None):
|
|
14
|
+
unique_approvers = sorted({str(approver) for approver in approvers if str(approver).strip()})
|
|
15
|
+
if len(unique_approvers) < self.required_approvals:
|
|
16
|
+
raise PermissionError(
|
|
17
|
+
"Deployment requires %d unique approvals" % self.required_approvals
|
|
18
|
+
)
|
|
19
|
+
if quality_report is not None and not quality_report.get("passed", False):
|
|
20
|
+
raise ValueError("Deployment quality gates did not pass")
|
|
21
|
+
for approver in unique_approvers:
|
|
22
|
+
self.registry.record_approval(task_name, model_version, approver, state=state)
|
|
23
|
+
self.registry.promote(task_name, model_version, state)
|
|
24
|
+
return {"status": "promoted", "task_name": task_name, "model_version": model_version, "state": state}
|
|
25
|
+
|
|
26
|
+
def rollback(self, task_name, state, model_version=None):
|
|
27
|
+
self.registry.rollback(task_name, state, model_version=model_version)
|
|
28
|
+
return {"status": "rolled_back", "task_name": task_name, "state": state, "model_version": model_version}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = ["DeploymentApprovalGate"]
|
SKSurrogate/eoa.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Configurable evolutionary optimization operators.
|
|
2
|
+
|
|
3
|
+
The :class:`EOA` coordinator evolves a supplied population of tuple-like
|
|
4
|
+
individuals. Fitness values are maximized by the built-in crossover and
|
|
5
|
+
elitism operators.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EOA(object):
|
|
10
|
+
"""Coordinate population initialization, evolution, and checkpointing.
|
|
11
|
+
|
|
12
|
+
``fitness`` receives an ``OrderedDict`` of individuals and must return an
|
|
13
|
+
``OrderedDict`` with numeric fitness values. Larger values are considered
|
|
14
|
+
better. Operators are classes; they are instantiated with the remaining
|
|
15
|
+
keyword arguments and called with this ``EOA`` instance.
|
|
16
|
+
|
|
17
|
+
:param population: Complete collection of possible individuals.
|
|
18
|
+
:param fitness: Callback that evaluates an ``OrderedDict`` of individuals.
|
|
19
|
+
:param init_pop: Parent initializer class; defaults to ``UniformRand``.
|
|
20
|
+
:param recomb: Recombination class; defaults to ``UniformCrossover``.
|
|
21
|
+
:param mutation: Mutation class; defaults to ``Mutation``.
|
|
22
|
+
:param termination: Termination class; defaults to ``MaxGenTermination``.
|
|
23
|
+
:param elitism: Elitism class; defaults to ``Elites``.
|
|
24
|
+
:param num_parents: Number of parents selected each generation. If omitted,
|
|
25
|
+
it is derived from ``parents_porp``.
|
|
26
|
+
:param parents_porp: Parent proportion used when ``num_parents`` is omitted;
|
|
27
|
+
defaults to ``0.1``.
|
|
28
|
+
:param elits_porp: Proportion used to calculate the elite count; defaults to
|
|
29
|
+
``0.2``.
|
|
30
|
+
:param mutation_prob: Probability of changing each individual element;
|
|
31
|
+
defaults to ``0.05``.
|
|
32
|
+
:param max_generation: Maximum generation count for the default termination
|
|
33
|
+
operator; defaults to ``50``.
|
|
34
|
+
:param genes: Complete gene list used by ``Mutation``. Inferred from the
|
|
35
|
+
population when omitted.
|
|
36
|
+
:param init_genes: Genes allowed in the first position during mutation.
|
|
37
|
+
:param term_genes: Genes allowed in the last position during mutation.
|
|
38
|
+
:param task_name: Checkpoint filename prefix; defaults to ``"EOA"``.
|
|
39
|
+
:param check_point: Directory prefix for checkpoints; defaults to ``"./"``.
|
|
40
|
+
The directory must already exist.
|
|
41
|
+
:param random_state: Optional seed for reproducible built-in operators.
|
|
42
|
+
"""
|
|
43
|
+
CHECKPOINT_VERSION = 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def __init__(self, population, fitness, **kwargs):
|
|
47
|
+
|
|
48
|
+
from collections import OrderedDict
|
|
49
|
+
import random
|
|
50
|
+
|
|
51
|
+
self.population = population
|
|
52
|
+
self.random_state = kwargs.pop("random_state", None)
|
|
53
|
+
self.rng = random.Random(self.random_state)
|
|
54
|
+
self.init_pop = kwargs.pop("init_pop", UniformRand)(**kwargs)
|
|
55
|
+
self.fitness = fitness
|
|
56
|
+
self.recomb = kwargs.pop("recomb", UniformCrossover)(**kwargs)
|
|
57
|
+
self.mutation = kwargs.pop("mutation", Mutation)(**kwargs)
|
|
58
|
+
self.termination = kwargs.pop("termination", MaxGenTermination)(**kwargs)
|
|
59
|
+
self.elitism = kwargs.pop("elitism", Elites)(**kwargs)
|
|
60
|
+
self.population_size = len(self.population)
|
|
61
|
+
self.parents_porp = kwargs.pop("parents_porp", 0.1)
|
|
62
|
+
self.num_parents = kwargs.pop(
|
|
63
|
+
"num_parents", 2 * int(self.population_size * self.parents_porp / 2.0)
|
|
64
|
+
)
|
|
65
|
+
if self.num_parents > self.population_size:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
"num_parents ({num_parents}) exceeds the population size "
|
|
68
|
+
"({population_size}). Narrow num_parents (or widen the search "
|
|
69
|
+
"space) so that at least num_parents candidates exist.".format(
|
|
70
|
+
num_parents=self.num_parents,
|
|
71
|
+
population_size=self.population_size,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
self.elits_porp = kwargs.pop("elits_porp", 0.2)
|
|
75
|
+
self.num_elites = int(self.elits_porp * self.num_parents)
|
|
76
|
+
self.mutation_prob = kwargs.pop("mutation_prob", 0.05)
|
|
77
|
+
self.max_generations = kwargs.pop("max_generation", 50)
|
|
78
|
+
self.generation_num = 0
|
|
79
|
+
self.genes = kwargs.pop("genes", [])
|
|
80
|
+
self.init_genes = kwargs.pop("init_genes", [])
|
|
81
|
+
self.term_genes = kwargs.pop("term_genes", [])
|
|
82
|
+
self.task_name = kwargs.pop("task_name", "EOA")
|
|
83
|
+
self.check_point = kwargs.pop("check_point", "./")
|
|
84
|
+
if not self.genes:
|
|
85
|
+
self.find_genes()
|
|
86
|
+
self.evals = OrderedDict([(_, None) for _ in self.population])
|
|
87
|
+
self.parents = OrderedDict()
|
|
88
|
+
self.children = OrderedDict()
|
|
89
|
+
|
|
90
|
+
def find_genes(self):
|
|
91
|
+
"""Infer mutation genes and positional gene lists from the population."""
|
|
92
|
+
for ind in self.population:
|
|
93
|
+
for e in ind:
|
|
94
|
+
if e not in self.genes:
|
|
95
|
+
self.genes.append(e)
|
|
96
|
+
if not self.init_genes:
|
|
97
|
+
self.init_genes = self.genes
|
|
98
|
+
if not self.term_genes:
|
|
99
|
+
self.term_genes = self.genes
|
|
100
|
+
|
|
101
|
+
def __save(self):
|
|
102
|
+
"""Serialize the current optimizer state to the ``.eoa`` checkpoint."""
|
|
103
|
+
from pickle import dumps
|
|
104
|
+
|
|
105
|
+
fl = open(self.check_point + self.task_name + ".eoa", "wb")
|
|
106
|
+
info = dict(
|
|
107
|
+
checkpoint_version=self.CHECKPOINT_VERSION,
|
|
108
|
+
population_size=self.population_size,
|
|
109
|
+
parents_porp=self.parents_porp,
|
|
110
|
+
num_parents=self.num_parents,
|
|
111
|
+
elits_porp=self.elits_porp,
|
|
112
|
+
num_elites=self.num_elites,
|
|
113
|
+
mutation_prob=self.mutation_prob,
|
|
114
|
+
max_generations=self.max_generations,
|
|
115
|
+
generation_num=self.generation_num,
|
|
116
|
+
genes=self.genes,
|
|
117
|
+
init_genes=self.init_genes,
|
|
118
|
+
term_genes=self.term_genes,
|
|
119
|
+
task_name=self.task_name,
|
|
120
|
+
check_point=self.check_point,
|
|
121
|
+
random_state=self.random_state,
|
|
122
|
+
rng_state=self.rng.getstate(),
|
|
123
|
+
evals=self.evals,
|
|
124
|
+
parents=self.parents,
|
|
125
|
+
children=self.children,
|
|
126
|
+
)
|
|
127
|
+
fl.write(dumps(info))
|
|
128
|
+
fl.close()
|
|
129
|
+
|
|
130
|
+
def __load(self):
|
|
131
|
+
"""Restore a matching checkpoint when one exists."""
|
|
132
|
+
from pickle import loads
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
fl = open(self.check_point + self.task_name + ".eoa", "rb")
|
|
136
|
+
info = loads(fl.read())
|
|
137
|
+
fl.close()
|
|
138
|
+
if info.get("checkpoint_version", self.CHECKPOINT_VERSION) != self.CHECKPOINT_VERSION:
|
|
139
|
+
raise ValueError("Unsupported EOA checkpoint version")
|
|
140
|
+
self.population_size = info["population_size"]
|
|
141
|
+
self.parents_porp = info["parents_porp"]
|
|
142
|
+
self.num_parents = info["num_parents"]
|
|
143
|
+
self.elits_porp = info["elits_porp"]
|
|
144
|
+
self.num_elites = info["num_elites"]
|
|
145
|
+
self.mutation_prob = info["mutation_prob"]
|
|
146
|
+
self.max_generations = info["max_generations"]
|
|
147
|
+
self.generation_num = info["generation_num"]
|
|
148
|
+
self.genes = info["genes"]
|
|
149
|
+
self.init_genes = info["init_genes"]
|
|
150
|
+
self.term_genes = info["term_genes"]
|
|
151
|
+
self.task_name = info["task_name"]
|
|
152
|
+
self.check_point = info["check_point"]
|
|
153
|
+
self.random_state = info.get("random_state", self.random_state)
|
|
154
|
+
if "rng_state" in info:
|
|
155
|
+
self.rng.setstate(info["rng_state"])
|
|
156
|
+
self.evals = info["evals"]
|
|
157
|
+
self.parents = info["parents"]
|
|
158
|
+
self.children = info["children"]
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
def __call__(self, *args, **kwargs):
|
|
163
|
+
"""Run generations until the configured termination operator returns true.
|
|
164
|
+
|
|
165
|
+
The method mutates ``parents``, ``children``, ``evals`` and
|
|
166
|
+
``generation_num`` in place. It returns ``None``; inspect ``evals`` or
|
|
167
|
+
``children`` after the run to retrieve results.
|
|
168
|
+
"""
|
|
169
|
+
self.parents = self.init_pop(self)
|
|
170
|
+
self.__load()
|
|
171
|
+
#tqdm = None
|
|
172
|
+
#pbar = None
|
|
173
|
+
#try:
|
|
174
|
+
# from tqdm import tqdm
|
|
175
|
+
#except ImportError:
|
|
176
|
+
# tqdm = None
|
|
177
|
+
#if tqdm is not None:
|
|
178
|
+
# pbar = tqdm(total=self.max_generations)
|
|
179
|
+
#pbar.update(self.generation_num)
|
|
180
|
+
while not self.termination(self):
|
|
181
|
+
print("Generation {g} of {t}".format(g=self.generation_num, t=self.max_generations))
|
|
182
|
+
self.__save()
|
|
183
|
+
self.generation_num += 1
|
|
184
|
+
self.parents = self.fitness(self.parents)
|
|
185
|
+
for _ in self.parents:
|
|
186
|
+
self.evals[_] = self.parents[_]
|
|
187
|
+
self.recomb(self)
|
|
188
|
+
self.mutation(self)
|
|
189
|
+
self.children = self.fitness(self.children)
|
|
190
|
+
for _ in self.children:
|
|
191
|
+
self.evals[_] = self.children[_]
|
|
192
|
+
self.elitism(self)
|
|
193
|
+
self.parents = self.children
|
|
194
|
+
#if tqdm is not None:
|
|
195
|
+
# pbar.update(1)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class UniformRand(object):
|
|
199
|
+
"""Select distinct initial parents uniformly from the population."""
|
|
200
|
+
|
|
201
|
+
def __init__(self, **kwargs):
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def __call__(self, ref, *args, **kwargs):
|
|
205
|
+
"""Return an ``OrderedDict`` containing ``ref.num_parents`` individuals."""
|
|
206
|
+
from collections import OrderedDict
|
|
207
|
+
import random
|
|
208
|
+
|
|
209
|
+
rng = getattr(ref, "rng", random)
|
|
210
|
+
indices = rng.sample(range(ref.population_size), ref.num_parents)
|
|
211
|
+
return OrderedDict(
|
|
212
|
+
[(ref.population[i], ref.evals[ref.population[i]]) for i in indices]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class MaxGenTermination(object):
|
|
217
|
+
"""Stop when ``ref.generation_num`` reaches ``ref.max_generations``."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, **kwargs):
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
def __call__(self, ref, *args, **kwargs):
|
|
223
|
+
"""Return ``True`` when the configured generation limit is reached."""
|
|
224
|
+
if ref.generation_num < ref.max_generations:
|
|
225
|
+
return False
|
|
226
|
+
return True
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class UniformCrossover(object):
|
|
230
|
+
"""Select fitness-weighted parent pairs and create tuple children.
|
|
231
|
+
|
|
232
|
+
The operator expects numeric parent fitness values and writes the
|
|
233
|
+
resulting children to ``ref.children``. Higher fitness receives greater
|
|
234
|
+
selection weight.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
def __init__(self, **kwargs):
|
|
238
|
+
self.fitnesses = []
|
|
239
|
+
self.fmin = 0.0
|
|
240
|
+
self.fmax = 0.0
|
|
241
|
+
self.kwargs = kwargs
|
|
242
|
+
self.mated = False
|
|
243
|
+
|
|
244
|
+
def scale(self, scrs):
|
|
245
|
+
"""Normalize fitness values into non-negative mating weights."""
|
|
246
|
+
self.fmin = min(scrs)
|
|
247
|
+
self.fmax = max(scrs)
|
|
248
|
+
lng = self.fmax - self.fmin
|
|
249
|
+
if lng == 0.0:
|
|
250
|
+
lng = 1.0
|
|
251
|
+
self.fitnesses = [(_ - self.fmin) / lng for _ in scrs]
|
|
252
|
+
|
|
253
|
+
def select_idx(self):
|
|
254
|
+
"""Select a parent index using the current mating weights."""
|
|
255
|
+
fsum = sum(self.fitnesses)
|
|
256
|
+
r = self.rng.uniform(0.0, fsum)
|
|
257
|
+
idx = 0
|
|
258
|
+
F = self.fitnesses[idx]
|
|
259
|
+
while F < r:
|
|
260
|
+
idx += 1
|
|
261
|
+
F += self.fitnesses[idx]
|
|
262
|
+
return idx
|
|
263
|
+
|
|
264
|
+
def pair(self):
|
|
265
|
+
"""Remove and return one fitness-weighted pair from the parent pool."""
|
|
266
|
+
p1 = self.parents.pop(-1)
|
|
267
|
+
self.fitnesses.pop(-1)
|
|
268
|
+
idx = self.select_idx()
|
|
269
|
+
p2 = self.parents.pop(idx)
|
|
270
|
+
self.fitnesses.pop(idx)
|
|
271
|
+
return p1, p2
|
|
272
|
+
|
|
273
|
+
def mate(self, p1, p2):
|
|
274
|
+
"""Exchange tuple suffixes from two parents and return two children."""
|
|
275
|
+
l1 = len(p1)
|
|
276
|
+
l2 = len(p2)
|
|
277
|
+
if l1 == 1 and l2 == 1:
|
|
278
|
+
c1 = (p1[0], p2[0])
|
|
279
|
+
c2 = (p2[0], p1[0])
|
|
280
|
+
return c1, c2
|
|
281
|
+
r = self.rng.randint(1, max(l1, l2))
|
|
282
|
+
cl1 = max(0, l1 - r)
|
|
283
|
+
cl2 = max(0, l2 - r)
|
|
284
|
+
c1l = list(p1)[:cl1]
|
|
285
|
+
c1r = list(p1)[cl1:]
|
|
286
|
+
c2l = list(p2)[:cl2]
|
|
287
|
+
c2r = list(p2)[cl2:]
|
|
288
|
+
c1 = tuple(c1l + c2r)
|
|
289
|
+
c2 = tuple(c2l + c1r)
|
|
290
|
+
self.mated = True
|
|
291
|
+
return c1, c2
|
|
292
|
+
|
|
293
|
+
def __call__(self, ref, *args, **kwargs):
|
|
294
|
+
"""Populate ``ref.children`` with recombined individuals."""
|
|
295
|
+
from collections import OrderedDict
|
|
296
|
+
|
|
297
|
+
self.rng = ref.rng
|
|
298
|
+
ref.parents = OrderedDict(sorted(ref.parents.items(), key=lambda x: x[1]))
|
|
299
|
+
self.parents = list(ref.parents.keys())
|
|
300
|
+
self.scale(list(ref.parents.values()))
|
|
301
|
+
children = []
|
|
302
|
+
while len(self.parents) > 1:
|
|
303
|
+
p1, p2 = self.pair()
|
|
304
|
+
c1, c2 = self.mate(p1, p2)
|
|
305
|
+
if c1 in ref.evals:
|
|
306
|
+
children += [c1]
|
|
307
|
+
if c2 in ref.evals:
|
|
308
|
+
children += [c2]
|
|
309
|
+
ref.children = OrderedDict([(c, ref.evals[c]) for c in children])
|
|
310
|
+
ref.children = ref.fitness(ref.children)
|
|
311
|
+
ref.children = OrderedDict(sorted(ref.children.items(), key=lambda x: x[1]))
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class Elites(object):
|
|
315
|
+
"""Preserve the highest-fitness parents among the next generation."""
|
|
316
|
+
|
|
317
|
+
def __init__(self, **kwargs):
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
def __call__(self, ref, *args, **kwargs):
|
|
321
|
+
"""Merge elite parents into ``ref.children`` in fitness order."""
|
|
322
|
+
from collections import OrderedDict
|
|
323
|
+
|
|
324
|
+
children = ref.children
|
|
325
|
+
parents = ref.parents
|
|
326
|
+
dif_ = ref.num_elites + len(parents) - len(children)
|
|
327
|
+
top_elites = list(parents.items())[-dif_:]
|
|
328
|
+
top_children = list(children.items())[dif_:]
|
|
329
|
+
ref.children = OrderedDict(
|
|
330
|
+
sorted(top_elites + top_children, key=lambda x: x[1])
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class Mutation(object):
|
|
335
|
+
"""Mutate child genes using the configured per-element probability."""
|
|
336
|
+
|
|
337
|
+
def __init__(self, **kwargs):
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
def __call__(self, ref, *args, **kwargs):
|
|
341
|
+
"""Replace genes in ``ref.children`` and remove empty mutations."""
|
|
342
|
+
from collections import OrderedDict
|
|
343
|
+
|
|
344
|
+
rng = ref.rng
|
|
345
|
+
mchildren = []
|
|
346
|
+
for chld in ref.children:
|
|
347
|
+
mchld = []
|
|
348
|
+
idx = 0
|
|
349
|
+
length = len(chld) - 1
|
|
350
|
+
for e in chld:
|
|
351
|
+
me = e
|
|
352
|
+
prb = rng.uniform(0, 1.0)
|
|
353
|
+
if prb <= ref.mutation_prob:
|
|
354
|
+
if idx == 0:
|
|
355
|
+
lng = len(ref.init_genes)
|
|
356
|
+
rdx = rng.randint(0, lng)
|
|
357
|
+
if rdx < lng:
|
|
358
|
+
me = ref.init_genes[rdx]
|
|
359
|
+
else:
|
|
360
|
+
me = ""
|
|
361
|
+
elif idx == length:
|
|
362
|
+
lng = len(ref.term_genes)
|
|
363
|
+
rdx = rng.randint(0, lng)
|
|
364
|
+
if rdx < lng:
|
|
365
|
+
me = ref.term_genes[rdx]
|
|
366
|
+
else:
|
|
367
|
+
me = ""
|
|
368
|
+
else:
|
|
369
|
+
lng = len(ref.genes)
|
|
370
|
+
rdx = rng.randint(0, lng)
|
|
371
|
+
if rdx < lng:
|
|
372
|
+
me = ref.genes[rdx]
|
|
373
|
+
else:
|
|
374
|
+
me = ""
|
|
375
|
+
if me != "":
|
|
376
|
+
mchld.append(me)
|
|
377
|
+
idx += 1
|
|
378
|
+
new_chld = tuple(mchld)
|
|
379
|
+
if new_chld in ref.evals:
|
|
380
|
+
mchildren.append((new_chld, ref.evals[new_chld]))
|
|
381
|
+
ref.children = OrderedDict(mchildren)
|