mosade 0.1.0__tar.gz
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.
- mosade-0.1.0/LICENSE +21 -0
- mosade-0.1.0/PKG-INFO +94 -0
- mosade-0.1.0/README.md +56 -0
- mosade-0.1.0/pyproject.toml +97 -0
- mosade-0.1.0/setup.cfg +4 -0
- mosade-0.1.0/src/mosade/__init__.py +3 -0
- mosade-0.1.0/src/mosade/algorithm/__init__.py +32 -0
- mosade-0.1.0/src/mosade/algorithm/adaptation.py +186 -0
- mosade-0.1.0/src/mosade/algorithm/archive.py +125 -0
- mosade-0.1.0/src/mosade/algorithm/decomposition.py +139 -0
- mosade-0.1.0/src/mosade/algorithm/moead.py +401 -0
- mosade-0.1.0/src/mosade/algorithm/mosade.py +1385 -0
- mosade-0.1.0/src/mosade/algorithm/nsga2.py +531 -0
- mosade-0.1.0/src/mosade/algorithm/pymoo_wrapper.py +279 -0
- mosade-0.1.0/src/mosade/algorithm/registry.py +99 -0
- mosade-0.1.0/src/mosade/algorithm/selection.py +353 -0
- mosade-0.1.0/src/mosade/algorithm/strategies.py +152 -0
- mosade-0.1.0/src/mosade/metrics/__init__.py +8 -0
- mosade-0.1.0/src/mosade/metrics/gd.py +45 -0
- mosade-0.1.0/src/mosade/metrics/hypervolume.py +122 -0
- mosade-0.1.0/src/mosade/metrics/igd.py +62 -0
- mosade-0.1.0/src/mosade/metrics/spread.py +95 -0
- mosade-0.1.0/src/mosade/problems/__init__.py +45 -0
- mosade-0.1.0/src/mosade/problems/base.py +142 -0
- mosade-0.1.0/src/mosade/problems/dascmop.py +387 -0
- mosade-0.1.0/src/mosade/problems/dtlz.py +240 -0
- mosade-0.1.0/src/mosade/problems/realworld_cre.py +303 -0
- mosade-0.1.0/src/mosade/problems/wfg.py +501 -0
- mosade-0.1.0/src/mosade/problems/zdt.py +119 -0
- mosade-0.1.0/src/mosade/runner/__init__.py +5 -0
- mosade-0.1.0/src/mosade/runner/experiment.py +1203 -0
- mosade-0.1.0/src/mosade/utils/__init__.py +6 -0
- mosade-0.1.0/src/mosade/utils/io.py +58 -0
- mosade-0.1.0/src/mosade/utils/logging.py +56 -0
- mosade-0.1.0/src/mosade/utils/seeding.py +31 -0
- mosade-0.1.0/src/mosade.egg-info/PKG-INFO +94 -0
- mosade-0.1.0/src/mosade.egg-info/SOURCES.txt +55 -0
- mosade-0.1.0/src/mosade.egg-info/dependency_links.txt +1 -0
- mosade-0.1.0/src/mosade.egg-info/requires.txt +16 -0
- mosade-0.1.0/src/mosade.egg-info/top_level.txt +1 -0
- mosade-0.1.0/tests/test_algorithm_regressions.py +79 -0
- mosade-0.1.0/tests/test_dascmop.py +195 -0
- mosade-0.1.0/tests/test_decomposition.py +85 -0
- mosade-0.1.0/tests/test_dtlz7.py +139 -0
- mosade-0.1.0/tests/test_epsilon_modes.py +131 -0
- mosade-0.1.0/tests/test_metrics.py +176 -0
- mosade-0.1.0/tests/test_mosade_telemetry_smoke.py +33 -0
- mosade-0.1.0/tests/test_problems.py +189 -0
- mosade-0.1.0/tests/test_pymoo_wrapper.py +74 -0
- mosade-0.1.0/tests/test_realworld_cre_shapes.py +92 -0
- mosade-0.1.0/tests/test_realworld_cre_smoke.py +54 -0
- mosade-0.1.0/tests/test_reference_point_and_metrics.py +18 -0
- mosade-0.1.0/tests/test_registry_presets.py +43 -0
- mosade-0.1.0/tests/test_selection_modes.py +38 -0
- mosade-0.1.0/tests/test_smoke.py +260 -0
- mosade-0.1.0/tests/test_unsupported_handling.py +74 -0
- mosade-0.1.0/tests/test_wfg.py +217 -0
mosade-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Li Jiawei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mosade-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mosade
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MOSADE: Multi-Objective Self-Adaptive Differential Evolution
|
|
5
|
+
Author: Li Jiawei
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Levvvi/MOSADE
|
|
8
|
+
Project-URL: Repository, https://github.com/Levvvi/MOSADE
|
|
9
|
+
Project-URL: Issues, https://github.com/Levvvi/MOSADE/issues
|
|
10
|
+
Keywords: multi-objective optimization,evolutionary algorithms,differential evolution,MOSADE,constrained optimization
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.24
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
31
|
+
Provides-Extra: analysis
|
|
32
|
+
Requires-Dist: matplotlib>=3.9; extra == "analysis"
|
|
33
|
+
Requires-Dist: scipy>=1.10; extra == "analysis"
|
|
34
|
+
Requires-Dist: pandas>=2.0; extra == "analysis"
|
|
35
|
+
Provides-Extra: baselines
|
|
36
|
+
Requires-Dist: pymoo>=0.6.1; extra == "baselines"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# MOSADE
|
|
40
|
+
|
|
41
|
+
MOSADE (Multi-Objective Self-Adaptive Differential Evolution) is a Python
|
|
42
|
+
framework for multi-objective optimisation with real-valued decision variables.
|
|
43
|
+
It combines multiple differential-evolution mutation strategies,
|
|
44
|
+
decomposition-based environmental selection, strategy-level parameter memories,
|
|
45
|
+
and epsilon-constraint handling for constrained problems.
|
|
46
|
+
|
|
47
|
+
## Repository Contents
|
|
48
|
+
|
|
49
|
+
- `src/mosade/algorithm/`: MOSADE, differential-evolution strategies,
|
|
50
|
+
decomposition, selection, adaptation, archive management, and baseline
|
|
51
|
+
implementations.
|
|
52
|
+
- `src/mosade/problems/`: ZDT, DTLZ, WFG, DASCMOP, and real-world CRE benchmark
|
|
53
|
+
problems.
|
|
54
|
+
- `src/mosade/metrics/`: hypervolume, IGD, IGD+, GD, spread, and spacing.
|
|
55
|
+
- `src/mosade/analysis/`: result merging, plotting, sensitivity analysis, and
|
|
56
|
+
statistical utilities.
|
|
57
|
+
- `configs/`: reproducible experiment configurations.
|
|
58
|
+
- `scripts/`: command-line runners and result post-processing utilities.
|
|
59
|
+
- `tests/`: unit and smoke tests.
|
|
60
|
+
- `figures/`, `tables/`, `validation/`: generated outputs and summary data.
|
|
61
|
+
|
|
62
|
+
Raw per-run experiment directories are written to `results/` and are not tracked
|
|
63
|
+
by Git because complete benchmark runs can be large.
|
|
64
|
+
|
|
65
|
+
## Installation
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -e ".[dev,analysis]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Python 3.10 or newer is required.
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
Run the default smoke experiment:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python scripts/run_experiment.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Run a specific configuration:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python scripts/run_experiment.py --config configs/smoke_test.yaml
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Run the test suite:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See `REPRODUCIBILITY.md` for the recommended workflow for reproducing generated
|
|
94
|
+
results.
|
mosade-0.1.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# MOSADE
|
|
2
|
+
|
|
3
|
+
MOSADE (Multi-Objective Self-Adaptive Differential Evolution) is a Python
|
|
4
|
+
framework for multi-objective optimisation with real-valued decision variables.
|
|
5
|
+
It combines multiple differential-evolution mutation strategies,
|
|
6
|
+
decomposition-based environmental selection, strategy-level parameter memories,
|
|
7
|
+
and epsilon-constraint handling for constrained problems.
|
|
8
|
+
|
|
9
|
+
## Repository Contents
|
|
10
|
+
|
|
11
|
+
- `src/mosade/algorithm/`: MOSADE, differential-evolution strategies,
|
|
12
|
+
decomposition, selection, adaptation, archive management, and baseline
|
|
13
|
+
implementations.
|
|
14
|
+
- `src/mosade/problems/`: ZDT, DTLZ, WFG, DASCMOP, and real-world CRE benchmark
|
|
15
|
+
problems.
|
|
16
|
+
- `src/mosade/metrics/`: hypervolume, IGD, IGD+, GD, spread, and spacing.
|
|
17
|
+
- `src/mosade/analysis/`: result merging, plotting, sensitivity analysis, and
|
|
18
|
+
statistical utilities.
|
|
19
|
+
- `configs/`: reproducible experiment configurations.
|
|
20
|
+
- `scripts/`: command-line runners and result post-processing utilities.
|
|
21
|
+
- `tests/`: unit and smoke tests.
|
|
22
|
+
- `figures/`, `tables/`, `validation/`: generated outputs and summary data.
|
|
23
|
+
|
|
24
|
+
Raw per-run experiment directories are written to `results/` and are not tracked
|
|
25
|
+
by Git because complete benchmark runs can be large.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e ".[dev,analysis]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Python 3.10 or newer is required.
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
Run the default smoke experiment:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python scripts/run_experiment.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run a specific configuration:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python scripts/run_experiment.py --config configs/smoke_test.yaml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run the test suite:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pytest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See `REPRODUCIBILITY.md` for the recommended workflow for reproducing generated
|
|
56
|
+
results.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mosade"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MOSADE: Multi-Objective Self-Adaptive Differential Evolution"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Li Jiawei" },
|
|
15
|
+
]
|
|
16
|
+
keywords = [
|
|
17
|
+
"multi-objective optimization",
|
|
18
|
+
"evolutionary algorithms",
|
|
19
|
+
"differential evolution",
|
|
20
|
+
"MOSADE",
|
|
21
|
+
"constrained optimization",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Science/Research",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Topic :: Scientific/Engineering",
|
|
33
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
34
|
+
]
|
|
35
|
+
dependencies = [
|
|
36
|
+
"numpy>=1.24",
|
|
37
|
+
"pyyaml>=6.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/Levvvi/MOSADE"
|
|
42
|
+
Repository = "https://github.com/Levvvi/MOSADE"
|
|
43
|
+
Issues = "https://github.com/Levvvi/MOSADE/issues"
|
|
44
|
+
|
|
45
|
+
[project.optional-dependencies]
|
|
46
|
+
dev = [
|
|
47
|
+
"build>=1.2",
|
|
48
|
+
"pytest>=7.0",
|
|
49
|
+
"pytest-cov>=4.0",
|
|
50
|
+
"ruff>=0.6.0",
|
|
51
|
+
]
|
|
52
|
+
analysis = [
|
|
53
|
+
"matplotlib>=3.9", # Axes.boxplot(tick_labels=) and Colormap.with_extremes()
|
|
54
|
+
"scipy>=1.10",
|
|
55
|
+
"pandas>=2.0",
|
|
56
|
+
]
|
|
57
|
+
baselines = [
|
|
58
|
+
"pymoo>=0.6.1",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Only the shippable `mosade` library under src/ is packaged into the wheel.
|
|
62
|
+
# The thesis tooling in experiments/ (the `mosade_experiments` package) is
|
|
63
|
+
# deliberately excluded from the distribution.
|
|
64
|
+
[tool.setuptools.packages.find]
|
|
65
|
+
where = ["src"]
|
|
66
|
+
include = ["mosade*"]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
# experiments/ is on the import path so the top-level suite (which keeps
|
|
71
|
+
# test_epsilon_modes.py) and `pytest experiments/tests/` can import the
|
|
72
|
+
# mosade_experiments tooling, even though it is never installed into the wheel.
|
|
73
|
+
pythonpath = ["src", "experiments"]
|
|
74
|
+
|
|
75
|
+
[tool.coverage.run]
|
|
76
|
+
source = ["src/mosade"]
|
|
77
|
+
|
|
78
|
+
[tool.coverage.report]
|
|
79
|
+
show_missing = true
|
|
80
|
+
skip_covered = false
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
exclude = [
|
|
84
|
+
"experiments",
|
|
85
|
+
"build",
|
|
86
|
+
"dist",
|
|
87
|
+
".eggs",
|
|
88
|
+
"*.egg-info",
|
|
89
|
+
".git",
|
|
90
|
+
".mypy_cache",
|
|
91
|
+
".pytest_cache",
|
|
92
|
+
".ruff_cache",
|
|
93
|
+
".venv",
|
|
94
|
+
"htmlcov",
|
|
95
|
+
]
|
|
96
|
+
line-length = 100
|
|
97
|
+
target-version = "py310"
|
mosade-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""MOSADE algorithm components.
|
|
2
|
+
|
|
3
|
+
``ALGORITHM_REGISTRY`` maps string type names (as used in the ``type:`` YAML
|
|
4
|
+
key) to callables so the experiment runner can instantiate algorithms by name
|
|
5
|
+
from YAML configs::
|
|
6
|
+
|
|
7
|
+
algorithms:
|
|
8
|
+
- name: MOSADE_run
|
|
9
|
+
type: MOSADE # optional; defaults to the name if omitted
|
|
10
|
+
pop_size: 100
|
|
11
|
+
- name: NSGA3_baseline
|
|
12
|
+
type: NSGA3
|
|
13
|
+
pop_size: 100
|
|
14
|
+
|
|
15
|
+
See :mod:`mosade.algorithm.registry` for the full registry and instructions on
|
|
16
|
+
adding new algorithms.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from mosade.algorithm.mosade import MOSADE, MOSADEResult
|
|
20
|
+
from mosade.algorithm.nsga2 import NSGA2
|
|
21
|
+
from mosade.algorithm.moead import MOEAD
|
|
22
|
+
from mosade.algorithm.pymoo_wrapper import PymooAlgorithm
|
|
23
|
+
from mosade.algorithm.registry import ALGORITHM_REGISTRY
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"MOSADE",
|
|
27
|
+
"MOSADEResult",
|
|
28
|
+
"NSGA2",
|
|
29
|
+
"MOEAD",
|
|
30
|
+
"PymooAlgorithm",
|
|
31
|
+
"ALGORITHM_REGISTRY",
|
|
32
|
+
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Self-adaptation mechanisms for MOSADE.
|
|
2
|
+
|
|
3
|
+
- Per-strategy LSHADE-style success memories for F and CR
|
|
4
|
+
- Credit-based sliding-window strategy selection probabilities
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from mosade.algorithm.strategies import NUM_STRATEGIES
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# LSHADE success memory
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LSHADEMemory:
|
|
23
|
+
"""Independent LSHADE-style success memory for one strategy.
|
|
24
|
+
|
|
25
|
+
Stores H historical mean values for F and CR, updated by weighted
|
|
26
|
+
Lehmer / weighted arithmetic means of successful parameter values.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, H: int = 5, init_F: float = 0.5, init_CR: float = 0.5) -> None:
|
|
30
|
+
self.H = H
|
|
31
|
+
self.M_F = np.full(H, init_F)
|
|
32
|
+
self.M_CR = np.full(H, init_CR)
|
|
33
|
+
self._k = 0 # circular write index
|
|
34
|
+
|
|
35
|
+
def sample(self, rng: np.random.Generator) -> tuple[float, float]:
|
|
36
|
+
"""Sample (F, CR) from the memory."""
|
|
37
|
+
r = rng.integers(self.H)
|
|
38
|
+
F = float(np.clip(rng.standard_cauchy() * 0.1 + self.M_F[r], 1e-6, 1.0))
|
|
39
|
+
CR = float(np.clip(rng.normal(self.M_CR[r], 0.1), 0.0, 1.0))
|
|
40
|
+
return F, CR
|
|
41
|
+
|
|
42
|
+
def sample_batch(
|
|
43
|
+
self, n: int, rng: np.random.Generator
|
|
44
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
45
|
+
"""Sample n (F, CR) pairs from the memory in one vectorised call.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
n : int
|
|
50
|
+
Number of pairs to sample.
|
|
51
|
+
rng : np.random.Generator
|
|
52
|
+
Random number generator.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
F_vals : ndarray, shape (n,)
|
|
57
|
+
CR_vals : ndarray, shape (n,)
|
|
58
|
+
"""
|
|
59
|
+
if n == 0:
|
|
60
|
+
return np.empty(0, dtype=float), np.empty(0, dtype=float)
|
|
61
|
+
r = rng.integers(self.H, size=n)
|
|
62
|
+
F_vals = np.clip(rng.standard_cauchy(n) * 0.1 + self.M_F[r], 1e-6, 1.0)
|
|
63
|
+
CR_vals = np.clip(rng.normal(self.M_CR[r], 0.1, size=n), 0.0, 1.0)
|
|
64
|
+
return F_vals, CR_vals
|
|
65
|
+
|
|
66
|
+
def update(self, S_F: list[float], S_CR: list[float], weights: list[float]) -> None:
|
|
67
|
+
"""Update memory with successful F/CR pairs.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
S_F, S_CR : lists of successful F and CR values
|
|
72
|
+
weights : improvement-based weights (same length as S_F/S_CR)
|
|
73
|
+
"""
|
|
74
|
+
if len(S_F) == 0:
|
|
75
|
+
return
|
|
76
|
+
w = np.array(weights)
|
|
77
|
+
w = w / (w.sum() + 1e-30) # normalise
|
|
78
|
+
|
|
79
|
+
sf = np.array(S_F)
|
|
80
|
+
scr = np.array(S_CR)
|
|
81
|
+
|
|
82
|
+
# Weighted Lehmer mean for F
|
|
83
|
+
mean_F = float(np.sum(w * sf**2) / (np.sum(w * sf) + 1e-30))
|
|
84
|
+
# Weighted arithmetic mean for CR
|
|
85
|
+
mean_CR = float(np.sum(w * scr))
|
|
86
|
+
|
|
87
|
+
self.M_F[self._k] = mean_F
|
|
88
|
+
self.M_CR[self._k] = mean_CR
|
|
89
|
+
self._k = (self._k + 1) % self.H
|
|
90
|
+
|
|
91
|
+
def reset(self, init_F: float = 0.5, init_CR: float = 0.5) -> None:
|
|
92
|
+
self.M_F[:] = init_F
|
|
93
|
+
self.M_CR[:] = init_CR
|
|
94
|
+
self._k = 0
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def mean_F(self) -> float:
|
|
98
|
+
"""Return the current mean of the F memory.
|
|
99
|
+
|
|
100
|
+
Used by run-history telemetry so we can verify that each strategy keeps
|
|
101
|
+
an independent success memory and that the memories are actually moving
|
|
102
|
+
over time.
|
|
103
|
+
"""
|
|
104
|
+
return float(np.mean(self.M_F))
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def mean_CR(self) -> float:
|
|
108
|
+
"""Return the current mean of the CR memory."""
|
|
109
|
+
return float(np.mean(self.M_CR))
|
|
110
|
+
|
|
111
|
+
def snapshot(self) -> dict[str, list[float] | float]:
|
|
112
|
+
"""Return a lightweight serialisable view of the memory state."""
|
|
113
|
+
return {
|
|
114
|
+
"M_F": self.M_F.tolist(),
|
|
115
|
+
"M_CR": self.M_CR.tolist(),
|
|
116
|
+
"mean_F": self.mean_F,
|
|
117
|
+
"mean_CR": self.mean_CR,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# Strategy selection via credit assignment
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class StrategySelector:
|
|
128
|
+
"""Credit-based adaptive strategy selection with sliding window.
|
|
129
|
+
|
|
130
|
+
Maintains selection probabilities π_k for each strategy, updated
|
|
131
|
+
from cumulative credit over a sliding window of recent generations.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
n_strategies: int = NUM_STRATEGIES
|
|
135
|
+
window_size: int = 50 # LP in the design doc
|
|
136
|
+
pi_min: float = 0.05
|
|
137
|
+
|
|
138
|
+
# internal
|
|
139
|
+
_pi: np.ndarray = field(init=False)
|
|
140
|
+
_credit_history: deque = field(init=False)
|
|
141
|
+
|
|
142
|
+
def __post_init__(self) -> None:
|
|
143
|
+
self._pi = np.full(self.n_strategies, 1.0 / self.n_strategies)
|
|
144
|
+
self._credit_history = deque(maxlen=self.window_size)
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def probabilities(self) -> np.ndarray:
|
|
148
|
+
return self._pi.copy()
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def credit_totals(self) -> np.ndarray:
|
|
152
|
+
"""Return sliding-window cumulative credits for logging/debugging."""
|
|
153
|
+
totals = np.zeros(self.n_strategies)
|
|
154
|
+
for c in self._credit_history:
|
|
155
|
+
totals += c
|
|
156
|
+
return totals
|
|
157
|
+
|
|
158
|
+
def select(self, rng: np.random.Generator) -> int:
|
|
159
|
+
"""Sample a strategy index using roulette-wheel on current probabilities."""
|
|
160
|
+
return int(rng.choice(self.n_strategies, p=self._pi))
|
|
161
|
+
|
|
162
|
+
def update(self, credits: np.ndarray) -> None:
|
|
163
|
+
"""Record per-strategy credits for this generation and update π.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
credits : ndarray, shape (n_strategies,)
|
|
168
|
+
Sum of credit values earned by each strategy this generation.
|
|
169
|
+
"""
|
|
170
|
+
self._credit_history.append(credits.copy())
|
|
171
|
+
|
|
172
|
+
# Sum over sliding window
|
|
173
|
+
totals = np.zeros(self.n_strategies)
|
|
174
|
+
for c in self._credit_history:
|
|
175
|
+
totals += c
|
|
176
|
+
|
|
177
|
+
if totals.sum() > 0:
|
|
178
|
+
self._pi = totals / totals.sum()
|
|
179
|
+
self._pi = np.maximum(self._pi, self.pi_min)
|
|
180
|
+
self._pi /= self._pi.sum() # re-normalise after floor
|
|
181
|
+
else:
|
|
182
|
+
self._pi[:] = 1.0 / self.n_strategies
|
|
183
|
+
|
|
184
|
+
def reset(self) -> None:
|
|
185
|
+
self._pi[:] = 1.0 / self.n_strategies
|
|
186
|
+
self._credit_history.clear()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""External archive: passive elite storage with crowding-based truncation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from mosade.algorithm.selection import dominates
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Archive:
|
|
11
|
+
"""Bounded external archive of nondominated feasible solutions.
|
|
12
|
+
|
|
13
|
+
Used for:
|
|
14
|
+
- Providing pbest candidates to mutation strategies S1 and S4.
|
|
15
|
+
- Final output of the algorithm.
|
|
16
|
+
|
|
17
|
+
Truncation is by crowding distance (removing least-crowded member).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, max_size: int) -> None:
|
|
21
|
+
self.max_size = max_size
|
|
22
|
+
self.X: list[np.ndarray] = []
|
|
23
|
+
self.F: list[np.ndarray] = []
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def size(self) -> int:
|
|
27
|
+
return len(self.X)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_empty(self) -> bool:
|
|
31
|
+
return self.size == 0
|
|
32
|
+
|
|
33
|
+
def get_objectives(self) -> np.ndarray:
|
|
34
|
+
"""Return objective matrix, shape (size, M)."""
|
|
35
|
+
if self.is_empty:
|
|
36
|
+
raise ValueError("Archive is empty.")
|
|
37
|
+
return np.array(self.F)
|
|
38
|
+
|
|
39
|
+
def get_decisions(self) -> np.ndarray:
|
|
40
|
+
"""Return decision matrix, shape (size, D)."""
|
|
41
|
+
if self.is_empty:
|
|
42
|
+
raise ValueError("Archive is empty.")
|
|
43
|
+
return np.array(self.X)
|
|
44
|
+
|
|
45
|
+
def update(self, X_new: np.ndarray, F_new: np.ndarray, CV_new: np.ndarray) -> None:
|
|
46
|
+
"""Add feasible nondominated solutions and truncate if needed.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
X_new : ndarray, shape (K, D)
|
|
51
|
+
F_new : ndarray, shape (K, M)
|
|
52
|
+
CV_new : ndarray, shape (K,)
|
|
53
|
+
"""
|
|
54
|
+
# Only consider feasible solutions
|
|
55
|
+
feas = CV_new <= 0.0
|
|
56
|
+
if not np.any(feas):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
X_cand = X_new[feas]
|
|
60
|
+
F_cand = F_new[feas]
|
|
61
|
+
|
|
62
|
+
# Skip candidates with non-finite objectives or decision variables.
|
|
63
|
+
finite = np.all(np.isfinite(F_cand), axis=1) & np.all(np.isfinite(X_cand), axis=1)
|
|
64
|
+
X_cand = X_cand[finite]
|
|
65
|
+
F_cand = F_cand[finite]
|
|
66
|
+
if len(X_cand) == 0:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
for i in range(len(X_cand)):
|
|
70
|
+
# Check if candidate is dominated by any archive member
|
|
71
|
+
dominated_by_archive = False
|
|
72
|
+
to_remove = []
|
|
73
|
+
for j in range(len(self.F)):
|
|
74
|
+
if dominates(np.array(self.F[j]), F_cand[i]):
|
|
75
|
+
dominated_by_archive = True
|
|
76
|
+
break
|
|
77
|
+
if dominates(F_cand[i], np.array(self.F[j])):
|
|
78
|
+
to_remove.append(j)
|
|
79
|
+
|
|
80
|
+
if dominated_by_archive:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Remove dominated archive members (in reverse order to preserve indices)
|
|
84
|
+
for j in sorted(to_remove, reverse=True):
|
|
85
|
+
self.X.pop(j)
|
|
86
|
+
self.F.pop(j)
|
|
87
|
+
|
|
88
|
+
self.X.append(X_cand[i].copy())
|
|
89
|
+
self.F.append(F_cand[i].copy())
|
|
90
|
+
|
|
91
|
+
# Truncate by crowding distance
|
|
92
|
+
while self.size > self.max_size:
|
|
93
|
+
self._remove_least_crowded()
|
|
94
|
+
|
|
95
|
+
def _remove_least_crowded(self) -> None:
|
|
96
|
+
"""Remove the archive member with smallest crowding distance."""
|
|
97
|
+
F_arr = np.array(self.F)
|
|
98
|
+
cd = _crowding_distance(F_arr)
|
|
99
|
+
worst = int(np.argmin(cd))
|
|
100
|
+
self.X.pop(worst)
|
|
101
|
+
self.F.pop(worst)
|
|
102
|
+
|
|
103
|
+
def random_member(self, rng: np.random.Generator) -> np.ndarray:
|
|
104
|
+
"""Return decision vector of a uniformly random archive member."""
|
|
105
|
+
idx = rng.integers(self.size)
|
|
106
|
+
return np.array(self.X[idx])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _crowding_distance(F: np.ndarray) -> np.ndarray:
|
|
110
|
+
"""Compute crowding distance for a set of objective vectors."""
|
|
111
|
+
N, M = F.shape
|
|
112
|
+
if N <= 2:
|
|
113
|
+
return np.full(N, np.inf)
|
|
114
|
+
|
|
115
|
+
cd = np.zeros(N)
|
|
116
|
+
for m in range(M):
|
|
117
|
+
order = np.argsort(F[:, m])
|
|
118
|
+
cd[order[0]] = np.inf
|
|
119
|
+
cd[order[-1]] = np.inf
|
|
120
|
+
f_range = F[order[-1], m] - F[order[0], m]
|
|
121
|
+
if f_range < 1e-30:
|
|
122
|
+
continue
|
|
123
|
+
for k in range(1, N - 1):
|
|
124
|
+
cd[order[k]] += (F[order[k + 1], m] - F[order[k - 1], m]) / f_range
|
|
125
|
+
return cd
|