orivex 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.
- orivex/__init__.py +49 -0
- orivex/api.py +75 -0
- orivex/capabilities.py +42 -0
- orivex/engine.py +192 -0
- orivex/features/__init__.py +1 -0
- orivex/features/distribution.py +212 -0
- orivex/features/fitness_distance.py +244 -0
- orivex/features/information_content.py +442 -0
- orivex/features/meta_model.py +375 -0
- orivex/features/nearest_better.py +290 -0
- orivex/normalization.py +41 -0
- orivex/options.py +41 -0
- orivex/planner.py +89 -0
- orivex/py.typed +0 -0
- orivex/registry.py +57 -0
- orivex/result.py +96 -0
- orivex/sample.py +133 -0
- orivex/specs.py +144 -0
- orivex/torch/__init__.py +19 -0
- orivex/torch/api.py +120 -0
- orivex/torch/engine.py +162 -0
- orivex/torch/features/__init__.py +1 -0
- orivex/torch/features/distribution.py +148 -0
- orivex/torch/features/fitness_distance.py +129 -0
- orivex/torch/features/meta_model.py +287 -0
- orivex/torch/normalization.py +28 -0
- orivex/torch/sample.py +155 -0
- orivex-0.1.0.dist-info/METADATA +152 -0
- orivex-0.1.0.dist-info/RECORD +32 -0
- orivex-0.1.0.dist-info/WHEEL +5 -0
- orivex-0.1.0.dist-info/licenses/LICENSE.txt +21 -0
- orivex-0.1.0.dist-info/top_level.txt +1 -0
orivex/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Better FLACCO: selective and verified exploratory landscape analysis."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from orivex.api import compute, list_features
|
|
6
|
+
from orivex.capabilities import AutogradSupport, FeatureCapability
|
|
7
|
+
from orivex.normalization import YNormalization
|
|
8
|
+
from orivex.result import BackendName, DeviceType, FloatingDType
|
|
9
|
+
from orivex.sample import LandscapeSample, ObjectiveSense, ObjectiveSenseName
|
|
10
|
+
from orivex.specs import (
|
|
11
|
+
CostModel,
|
|
12
|
+
CostTier,
|
|
13
|
+
FeatureSpec,
|
|
14
|
+
InputRequirement,
|
|
15
|
+
InvarianceBehavior,
|
|
16
|
+
InvarianceClaim,
|
|
17
|
+
MetricKind,
|
|
18
|
+
Reference,
|
|
19
|
+
Transformation,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
__version__ = version("orivex")
|
|
24
|
+
except PackageNotFoundError: # Running directly from a source checkout.
|
|
25
|
+
__version__ = "0.0.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AutogradSupport",
|
|
29
|
+
"BackendName",
|
|
30
|
+
"CostModel",
|
|
31
|
+
"CostTier",
|
|
32
|
+
"DeviceType",
|
|
33
|
+
"FeatureCapability",
|
|
34
|
+
"FeatureSpec",
|
|
35
|
+
"FloatingDType",
|
|
36
|
+
"InputRequirement",
|
|
37
|
+
"InvarianceBehavior",
|
|
38
|
+
"InvarianceClaim",
|
|
39
|
+
"LandscapeSample",
|
|
40
|
+
"MetricKind",
|
|
41
|
+
"ObjectiveSense",
|
|
42
|
+
"ObjectiveSenseName",
|
|
43
|
+
"Reference",
|
|
44
|
+
"Transformation",
|
|
45
|
+
"YNormalization",
|
|
46
|
+
"__version__",
|
|
47
|
+
"compute",
|
|
48
|
+
"list_features",
|
|
49
|
+
]
|
orivex/api.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Public convenience API backed by the built-in feature engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from orivex.engine import Engine
|
|
8
|
+
from orivex.features.distribution import FEATURES as DISTRIBUTION_FEATURES
|
|
9
|
+
from orivex.features.distribution import INTERMEDIATES as DISTRIBUTION_INTERMEDIATES
|
|
10
|
+
from orivex.features.fitness_distance import FEATURES as FITNESS_DISTANCE_FEATURES
|
|
11
|
+
from orivex.features.fitness_distance import INTERMEDIATES as FITNESS_DISTANCE_INTERMEDIATES
|
|
12
|
+
from orivex.features.information_content import FEATURES as INFORMATION_CONTENT_FEATURES
|
|
13
|
+
from orivex.features.information_content import INTERMEDIATES as INFORMATION_CONTENT_INTERMEDIATES
|
|
14
|
+
from orivex.features.meta_model import FEATURES as META_MODEL_FEATURES
|
|
15
|
+
from orivex.features.meta_model import INTERMEDIATES as META_MODEL_INTERMEDIATES
|
|
16
|
+
from orivex.features.nearest_better import FEATURES as NEAREST_BETTER_FEATURES
|
|
17
|
+
from orivex.features.nearest_better import INTERMEDIATES as NEAREST_BETTER_INTERMEDIATES
|
|
18
|
+
from orivex.normalization import YNormalization
|
|
19
|
+
from orivex.options import FeatureOptions
|
|
20
|
+
from orivex.result import ComputationResult
|
|
21
|
+
from orivex.sample import LandscapeSample
|
|
22
|
+
from orivex.specs import FeatureSpec
|
|
23
|
+
|
|
24
|
+
DEFAULT_ENGINE = Engine(
|
|
25
|
+
DISTRIBUTION_FEATURES
|
|
26
|
+
+ META_MODEL_FEATURES
|
|
27
|
+
+ INFORMATION_CONTENT_FEATURES
|
|
28
|
+
+ NEAREST_BETTER_FEATURES
|
|
29
|
+
+ FITNESS_DISTANCE_FEATURES,
|
|
30
|
+
DISTRIBUTION_INTERMEDIATES
|
|
31
|
+
+ META_MODEL_INTERMEDIATES
|
|
32
|
+
+ INFORMATION_CONTENT_INTERMEDIATES
|
|
33
|
+
+ NEAREST_BETTER_INTERMEDIATES
|
|
34
|
+
+ FITNESS_DISTANCE_INTERMEDIATES,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def compute(
|
|
39
|
+
sample: LandscapeSample,
|
|
40
|
+
features: str | tuple[str, ...] | list[str],
|
|
41
|
+
*,
|
|
42
|
+
rng: np.random.Generator | None = None,
|
|
43
|
+
workers: int = 1,
|
|
44
|
+
y_normalization: YNormalization = "minmax",
|
|
45
|
+
options: FeatureOptions | None = None,
|
|
46
|
+
) -> ComputationResult:
|
|
47
|
+
"""Compute features from canonical objectives, min-max normalized by default.
|
|
48
|
+
|
|
49
|
+
``y_normalization`` is ``"minmax"`` (observed range) or ``None`` (raw canonical
|
|
50
|
+
objectives). Normalized constant objectives map to zero. The original sample is
|
|
51
|
+
preserved; preprocessing is recorded
|
|
52
|
+
separately from mathematical feature definitions in result metadata.
|
|
53
|
+
|
|
54
|
+
``options`` is a nested mapping keyed by feature group, for example
|
|
55
|
+
``{"fitness_distance": {"proportion_of_best": 0.25}}``. Omit it to use defaults.
|
|
56
|
+
Unknown groups and option names are rejected. Fitness-distance features always use
|
|
57
|
+
Euclidean distances to the best selected observation. Effective options are recorded
|
|
58
|
+
in ``result.metadata.options`` as an immutable snapshot.
|
|
59
|
+
|
|
60
|
+
``workers=1`` is the predictable default. Pass ``-1`` to let supporting kernels use all
|
|
61
|
+
available CPUs, or a positive integer to set an upper worker count. Kernels that do not
|
|
62
|
+
support parallel execution ignore this setting.
|
|
63
|
+
"""
|
|
64
|
+
return DEFAULT_ENGINE.compute(
|
|
65
|
+
sample,
|
|
66
|
+
features,
|
|
67
|
+
rng=rng,
|
|
68
|
+
workers=workers,
|
|
69
|
+
y_normalization=y_normalization,
|
|
70
|
+
options=options,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def list_features() -> tuple[FeatureSpec, ...]:
|
|
75
|
+
return tuple(DEFAULT_ENGINE.registry.get(name) for name in DEFAULT_ENGINE.registry.names())
|
orivex/capabilities.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Backend-specific feature implementation capabilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal, TypeAlias
|
|
7
|
+
|
|
8
|
+
from orivex.result import BackendName, DeviceType, FloatingDType
|
|
9
|
+
|
|
10
|
+
AutogradSupport: TypeAlias = Literal["smooth", "piecewise", "none"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class FeatureCapability:
|
|
15
|
+
"""Execution properties that belong to an implementation, not its mathematics."""
|
|
16
|
+
|
|
17
|
+
feature_name: str
|
|
18
|
+
backend: BackendName
|
|
19
|
+
autograd: AutogradSupport
|
|
20
|
+
devices: tuple[DeviceType, ...]
|
|
21
|
+
dtypes: tuple[FloatingDType, ...]
|
|
22
|
+
notes: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
if not self.feature_name:
|
|
26
|
+
raise ValueError("feature name must not be empty")
|
|
27
|
+
if self.backend not in ("numpy", "torch"):
|
|
28
|
+
raise ValueError(f"unsupported backend: {self.backend!r}")
|
|
29
|
+
if self.autograd not in ("smooth", "piecewise", "none"):
|
|
30
|
+
raise ValueError(f"unsupported autograd support: {self.autograd!r}")
|
|
31
|
+
if not self.devices:
|
|
32
|
+
raise ValueError("at least one device type is required")
|
|
33
|
+
if not self.dtypes:
|
|
34
|
+
raise ValueError("at least one floating dtype is required")
|
|
35
|
+
if any(device not in ("cpu", "cuda", "mps") for device in self.devices):
|
|
36
|
+
raise ValueError(f"unsupported device types: {self.devices!r}")
|
|
37
|
+
if any(dtype not in ("float32", "float64") for dtype in self.dtypes):
|
|
38
|
+
raise ValueError(f"unsupported floating dtypes: {self.dtypes!r}")
|
|
39
|
+
if len(set(self.devices)) != len(self.devices):
|
|
40
|
+
raise ValueError("device types must be unique")
|
|
41
|
+
if len(set(self.dtypes)) != len(self.dtypes):
|
|
42
|
+
raise ValueError("floating dtypes must be unique")
|
orivex/engine.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Execution of planned feature definitions with shared intermediate caching."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from orivex.normalization import YNormalization, normalize_objectives
|
|
13
|
+
from orivex.options import FeatureOptions, resolve_options
|
|
14
|
+
from orivex.planner import IntermediateSpec, Planner
|
|
15
|
+
from orivex.registry import FeatureRegistry
|
|
16
|
+
from orivex.result import ComputationResult, ExecutionMetadata, FeatureStatus, FeatureValue
|
|
17
|
+
from orivex.sample import LandscapeSample
|
|
18
|
+
from orivex.specs import FeatureSpec, InputRequirement
|
|
19
|
+
|
|
20
|
+
IntermediateValue = object
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FeatureUnavailable(ValueError):
|
|
24
|
+
"""A feature is mathematically undefined for an otherwise valid sample."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Numerical failures that are expected for otherwise valid samples (overflow, underflow, division
|
|
28
|
+
# by an underflowed quantity, singular designs). These are isolated per feature as INVALID results.
|
|
29
|
+
# Programming errors -- TypeError, KeyError, IndexError, AttributeError, and the like -- are not
|
|
30
|
+
# listed and therefore still propagate so that bugs remain distinguishable from numerical failures.
|
|
31
|
+
EXPECTED_NUMERICAL_ERRORS: tuple[type[Exception], ...] = (
|
|
32
|
+
FeatureUnavailable,
|
|
33
|
+
ArithmeticError,
|
|
34
|
+
np.linalg.LinAlgError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class _IntermediateFailure:
|
|
40
|
+
"""Marks an intermediate that raised an expected numerical failure.
|
|
41
|
+
|
|
42
|
+
Stored in place of a value so that features depending on it (directly or transitively) surface
|
|
43
|
+
the same failure as an INVALID status instead of terminating the whole request.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
message: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class ComputationContext:
|
|
51
|
+
sample: LandscapeSample
|
|
52
|
+
intermediates: Mapping[str, IntermediateValue]
|
|
53
|
+
rng: np.random.Generator | None
|
|
54
|
+
workers: int = 1
|
|
55
|
+
objective_y: np.ndarray | None = None
|
|
56
|
+
options: FeatureOptions = field(default_factory=resolve_options)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def y(self) -> np.ndarray:
|
|
60
|
+
"""Canonical objectives after the computation's declared preprocessing."""
|
|
61
|
+
return self.sample.minimization_y if self.objective_y is None else self.objective_y
|
|
62
|
+
|
|
63
|
+
def intermediate(self, name: str) -> IntermediateValue:
|
|
64
|
+
try:
|
|
65
|
+
value = self.intermediates[name]
|
|
66
|
+
except KeyError as error:
|
|
67
|
+
raise RuntimeError(f"intermediate was not planned: {name}") from error
|
|
68
|
+
if isinstance(value, _IntermediateFailure):
|
|
69
|
+
raise FeatureUnavailable(value.message)
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
FeatureCalculator = Callable[[ComputationContext], float | int]
|
|
74
|
+
IntermediateCalculator = Callable[[ComputationContext], IntermediateValue]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class FeatureDefinition:
|
|
79
|
+
spec: FeatureSpec
|
|
80
|
+
calculate: FeatureCalculator
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True, slots=True)
|
|
84
|
+
class IntermediateDefinition:
|
|
85
|
+
spec: IntermediateSpec
|
|
86
|
+
calculate: IntermediateCalculator
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Engine:
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
features: tuple[FeatureDefinition, ...],
|
|
93
|
+
intermediates: tuple[IntermediateDefinition, ...],
|
|
94
|
+
) -> None:
|
|
95
|
+
self._feature_definitions: dict[str, FeatureDefinition] = {}
|
|
96
|
+
for definition in features:
|
|
97
|
+
if definition.spec.name in self._feature_definitions:
|
|
98
|
+
raise ValueError(f"feature definition already registered: {definition.spec.name}")
|
|
99
|
+
self._feature_definitions[definition.spec.name] = definition
|
|
100
|
+
|
|
101
|
+
self._intermediate_definitions: dict[str, IntermediateDefinition] = {}
|
|
102
|
+
for definition in intermediates:
|
|
103
|
+
if definition.spec.name in self._intermediate_definitions:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"intermediate definition already registered: {definition.spec.name}"
|
|
106
|
+
)
|
|
107
|
+
self._intermediate_definitions[definition.spec.name] = definition
|
|
108
|
+
|
|
109
|
+
self.registry = FeatureRegistry(tuple(item.spec for item in features))
|
|
110
|
+
self.planner = Planner(
|
|
111
|
+
self.registry,
|
|
112
|
+
tuple(item.spec for item in intermediates),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def compute(
|
|
116
|
+
self,
|
|
117
|
+
sample: LandscapeSample,
|
|
118
|
+
features: str | tuple[str, ...] | list[str],
|
|
119
|
+
*,
|
|
120
|
+
rng: np.random.Generator | None = None,
|
|
121
|
+
workers: int = 1,
|
|
122
|
+
y_normalization: YNormalization = "minmax",
|
|
123
|
+
options: FeatureOptions | None = None,
|
|
124
|
+
) -> ComputationResult:
|
|
125
|
+
options = resolve_options(options)
|
|
126
|
+
if workers == 0 or workers < -1:
|
|
127
|
+
raise ValueError("workers must be -1 or a positive integer")
|
|
128
|
+
sample.validate_unchanged()
|
|
129
|
+
started = time.perf_counter()
|
|
130
|
+
plan = self.planner.plan(features)
|
|
131
|
+
if InputRequirement.RNG in plan.requirements and rng is None:
|
|
132
|
+
raise ValueError("the requested feature plan requires an explicit numpy Generator")
|
|
133
|
+
|
|
134
|
+
objective_y, constant = normalize_objectives(sample.minimization_y, y_normalization)
|
|
135
|
+
|
|
136
|
+
cache: dict[str, IntermediateValue] = {}
|
|
137
|
+
for intermediate in plan.intermediates:
|
|
138
|
+
context = ComputationContext(
|
|
139
|
+
sample,
|
|
140
|
+
MappingProxyType(cache),
|
|
141
|
+
rng,
|
|
142
|
+
workers,
|
|
143
|
+
objective_y,
|
|
144
|
+
options,
|
|
145
|
+
)
|
|
146
|
+
try:
|
|
147
|
+
cache[intermediate.name] = self._intermediate_definitions[
|
|
148
|
+
intermediate.name
|
|
149
|
+
].calculate(context)
|
|
150
|
+
except EXPECTED_NUMERICAL_ERRORS as error:
|
|
151
|
+
cache[intermediate.name] = _IntermediateFailure(str(error))
|
|
152
|
+
|
|
153
|
+
context = ComputationContext(
|
|
154
|
+
sample,
|
|
155
|
+
MappingProxyType(cache),
|
|
156
|
+
rng,
|
|
157
|
+
workers,
|
|
158
|
+
objective_y,
|
|
159
|
+
options,
|
|
160
|
+
)
|
|
161
|
+
values: dict[str, FeatureValue] = {}
|
|
162
|
+
for spec in plan.features:
|
|
163
|
+
definition = self._feature_definitions[spec.name]
|
|
164
|
+
try:
|
|
165
|
+
value = definition.calculate(context)
|
|
166
|
+
if not np.isfinite(value):
|
|
167
|
+
raise FeatureUnavailable("definition produced a non-finite value")
|
|
168
|
+
values[spec.name] = FeatureValue(
|
|
169
|
+
value=value,
|
|
170
|
+
status=FeatureStatus.OK,
|
|
171
|
+
definition=spec.definition,
|
|
172
|
+
)
|
|
173
|
+
except EXPECTED_NUMERICAL_ERRORS as error:
|
|
174
|
+
values[spec.name] = FeatureValue(
|
|
175
|
+
value=None,
|
|
176
|
+
status=FeatureStatus.INVALID,
|
|
177
|
+
definition=spec.definition,
|
|
178
|
+
message=str(error),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
metadata = ExecutionMetadata(
|
|
182
|
+
sample_fingerprint=sample.fingerprint,
|
|
183
|
+
requested_features=plan.feature_names,
|
|
184
|
+
computed_intermediates=plan.intermediate_names,
|
|
185
|
+
runtime_seconds=time.perf_counter() - started,
|
|
186
|
+
additional_objective_evaluations=0,
|
|
187
|
+
workers=workers,
|
|
188
|
+
y_normalization=y_normalization,
|
|
189
|
+
constant_objective=constant,
|
|
190
|
+
options=options,
|
|
191
|
+
)
|
|
192
|
+
return ComputationResult(values, metadata)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in feature definitions."""
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Objective-value distribution features compatible with flacco type-3 estimators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from orivex.engine import (
|
|
10
|
+
ComputationContext,
|
|
11
|
+
FeatureDefinition,
|
|
12
|
+
FeatureUnavailable,
|
|
13
|
+
IntermediateDefinition,
|
|
14
|
+
)
|
|
15
|
+
from orivex.planner import IntermediateSpec
|
|
16
|
+
from orivex.specs import (
|
|
17
|
+
CostModel,
|
|
18
|
+
CostTier,
|
|
19
|
+
FeatureSpec,
|
|
20
|
+
InputRequirement,
|
|
21
|
+
InvarianceBehavior,
|
|
22
|
+
InvarianceClaim,
|
|
23
|
+
MetricKind,
|
|
24
|
+
Reference,
|
|
25
|
+
Transformation,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
REFERENCE = Reference(
|
|
29
|
+
citation="Mersmann et al. (2011), Exploratory Landscape Analysis",
|
|
30
|
+
doi="10.1145/2001576.2001690",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class CenteredObjectives:
|
|
36
|
+
observations: int
|
|
37
|
+
values: np.ndarray
|
|
38
|
+
scale: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def centered_objectives(context: ComputationContext) -> CenteredObjectives:
|
|
42
|
+
"""Mean-centered objectives rescaled to unit maximum magnitude.
|
|
43
|
+
|
|
44
|
+
The type-3 skewness and kurtosis are invariant to a positive scaling of the deviations, so the
|
|
45
|
+
shared moments are accumulated from ``(y - mean) / max|y - mean|`` instead of the raw
|
|
46
|
+
deviations. The scale cancels analytically in every dependent feature, which keeps the moment
|
|
47
|
+
sums bounded by the observation count and therefore representable at any finite objective scale.
|
|
48
|
+
``scale`` is ``0.0`` exactly for a constant objective.
|
|
49
|
+
"""
|
|
50
|
+
y = context.y
|
|
51
|
+
deviations = y - np.mean(y)
|
|
52
|
+
# Constancy is decided on the objectives directly: a genuinely constant objective can still
|
|
53
|
+
# acquire tiny nonzero deviations through mean rounding, and rescaling would otherwise amplify
|
|
54
|
+
# that noise into apparently valid moments.
|
|
55
|
+
constant = y.size == 0 or bool(np.min(y) == np.max(y))
|
|
56
|
+
scale = 0.0 if constant else float(np.max(np.abs(deviations)))
|
|
57
|
+
values = deviations / scale if scale > 0.0 else np.zeros_like(deviations)
|
|
58
|
+
values.flags.writeable = False
|
|
59
|
+
return CenteredObjectives(observations=y.size, values=values, scale=scale)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
CENTERED = IntermediateDefinition(
|
|
63
|
+
spec=IntermediateSpec(
|
|
64
|
+
name="y.centered",
|
|
65
|
+
dependencies=(),
|
|
66
|
+
requirements=frozenset({InputRequirement.Y}),
|
|
67
|
+
),
|
|
68
|
+
calculate=centered_objectives,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _centered(context: ComputationContext) -> CenteredObjectives:
|
|
73
|
+
value = context.intermediate("y.centered")
|
|
74
|
+
if not isinstance(value, CenteredObjectives):
|
|
75
|
+
raise TypeError("y.centered has an invalid runtime type")
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sum2(context: ComputationContext) -> float:
|
|
80
|
+
centered = _centered(context).values
|
|
81
|
+
return float(centered @ centered)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def sum3(context: ComputationContext) -> float:
|
|
85
|
+
return float(np.sum(_centered(context).values ** 3))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def sum4(context: ComputationContext) -> float:
|
|
89
|
+
return float(np.sum(_centered(context).values ** 4))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
SUM2 = IntermediateDefinition(
|
|
93
|
+
IntermediateSpec("y.sum2", ("y.centered",), frozenset()),
|
|
94
|
+
sum2,
|
|
95
|
+
)
|
|
96
|
+
SUM3 = IntermediateDefinition(
|
|
97
|
+
IntermediateSpec("y.sum3", ("y.centered",), frozenset()),
|
|
98
|
+
sum3,
|
|
99
|
+
)
|
|
100
|
+
SUM4 = IntermediateDefinition(
|
|
101
|
+
IntermediateSpec("y.sum4", ("y.centered",), frozenset()),
|
|
102
|
+
sum4,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sum(context: ComputationContext, name: str) -> float:
|
|
107
|
+
value = context.intermediate(name)
|
|
108
|
+
if not isinstance(value, float):
|
|
109
|
+
raise TypeError(f"{name} has an invalid runtime type")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def skewness_type3(context: ComputationContext) -> float:
|
|
114
|
+
centered = _centered(context)
|
|
115
|
+
second = _sum(context, "y.sum2")
|
|
116
|
+
third = _sum(context, "y.sum3")
|
|
117
|
+
if centered.observations < 3:
|
|
118
|
+
raise FeatureUnavailable("type-3 skewness requires at least 3 observations")
|
|
119
|
+
if centered.scale == 0.0:
|
|
120
|
+
raise FeatureUnavailable("skewness is undefined for constant objective values")
|
|
121
|
+
n = centered.observations
|
|
122
|
+
type1 = np.sqrt(n) * third / second**1.5
|
|
123
|
+
return float(type1 * ((n - 1) / n) ** 1.5)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def kurtosis_type3(context: ComputationContext) -> float:
|
|
127
|
+
centered = _centered(context)
|
|
128
|
+
second = _sum(context, "y.sum2")
|
|
129
|
+
fourth = _sum(context, "y.sum4")
|
|
130
|
+
if centered.observations < 4:
|
|
131
|
+
raise FeatureUnavailable("type-3 kurtosis requires at least 4 observations")
|
|
132
|
+
if centered.scale == 0.0:
|
|
133
|
+
raise FeatureUnavailable("kurtosis is undefined for constant objective values")
|
|
134
|
+
n = centered.observations
|
|
135
|
+
ratio = n * fourth / second**2
|
|
136
|
+
return float(ratio * (1.0 - 1.0 / n) ** 2 - 3.0)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
COMMON_INVARIANCES = (
|
|
140
|
+
InvarianceClaim(
|
|
141
|
+
Transformation.ROW_PERMUTATION,
|
|
142
|
+
InvarianceBehavior.INVARIANT,
|
|
143
|
+
conditions="paired finite observations",
|
|
144
|
+
),
|
|
145
|
+
InvarianceClaim(
|
|
146
|
+
Transformation.Y_TRANSLATION,
|
|
147
|
+
InvarianceBehavior.INVARIANT,
|
|
148
|
+
conditions="finite y with non-zero variance",
|
|
149
|
+
),
|
|
150
|
+
InvarianceClaim(
|
|
151
|
+
Transformation.Y_POSITIVE_SCALING,
|
|
152
|
+
InvarianceBehavior.INVARIANT,
|
|
153
|
+
conditions="finite positive scale and non-zero y variance",
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
SKEWNESS = FeatureDefinition(
|
|
159
|
+
spec=FeatureSpec(
|
|
160
|
+
name="ela_distr.skewness",
|
|
161
|
+
group="ela_distr",
|
|
162
|
+
kind=MetricKind.LANDSCAPE,
|
|
163
|
+
definition="flacco-type3-v1",
|
|
164
|
+
summary="Type-3 sample skewness of objective observations under minimization convention.",
|
|
165
|
+
requirements=frozenset({InputRequirement.Y}),
|
|
166
|
+
intermediates=("y.sum2", "y.sum3"),
|
|
167
|
+
cost=CostModel(CostTier.SAMPLE_ONLY, cpu="O(n)", memory="O(n) shared"),
|
|
168
|
+
deterministic=True,
|
|
169
|
+
invariances=(
|
|
170
|
+
*COMMON_INVARIANCES,
|
|
171
|
+
InvarianceClaim(
|
|
172
|
+
Transformation.OBJECTIVE_SENSE_REVERSAL,
|
|
173
|
+
InvarianceBehavior.INVARIANT,
|
|
174
|
+
conditions="negate y and reverse the declared objective sense together",
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
references=(REFERENCE,),
|
|
178
|
+
legacy_names=("ela_distr.skewness",),
|
|
179
|
+
minimum_observations=3,
|
|
180
|
+
),
|
|
181
|
+
calculate=skewness_type3,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
KURTOSIS = FeatureDefinition(
|
|
186
|
+
spec=FeatureSpec(
|
|
187
|
+
name="ela_distr.kurtosis",
|
|
188
|
+
group="ela_distr",
|
|
189
|
+
kind=MetricKind.LANDSCAPE,
|
|
190
|
+
definition="flacco-type3-v1",
|
|
191
|
+
summary="Type-3 excess sample kurtosis of objective observations.",
|
|
192
|
+
requirements=frozenset({InputRequirement.Y}),
|
|
193
|
+
intermediates=("y.sum2", "y.sum4"),
|
|
194
|
+
cost=CostModel(CostTier.SAMPLE_ONLY, cpu="O(n)", memory="O(n) shared"),
|
|
195
|
+
deterministic=True,
|
|
196
|
+
invariances=(
|
|
197
|
+
*COMMON_INVARIANCES,
|
|
198
|
+
InvarianceClaim(
|
|
199
|
+
Transformation.OBJECTIVE_SENSE_REVERSAL,
|
|
200
|
+
InvarianceBehavior.INVARIANT,
|
|
201
|
+
conditions="negate y and reverse the declared objective sense together",
|
|
202
|
+
),
|
|
203
|
+
),
|
|
204
|
+
references=(REFERENCE,),
|
|
205
|
+
legacy_names=("ela_distr.kurtosis",),
|
|
206
|
+
minimum_observations=4,
|
|
207
|
+
),
|
|
208
|
+
calculate=kurtosis_type3,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
FEATURES = (SKEWNESS, KURTOSIS)
|
|
212
|
+
INTERMEDIATES = (CENTERED, SUM2, SUM3, SUM4)
|