rtide-optimizer 1.0.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.
- rtide_optimizer-1.0.0/LICENSE +21 -0
- rtide_optimizer-1.0.0/PKG-INFO +192 -0
- rtide_optimizer-1.0.0/README.md +174 -0
- rtide_optimizer-1.0.0/pyproject.toml +28 -0
- rtide_optimizer-1.0.0/python/RTIDE/__init__.py +3 -0
- rtide_optimizer-1.0.0/python/RTIDE/optimizer.py +189 -0
- rtide_optimizer-1.0.0/python/rtide_optimizer.egg-info/PKG-INFO +192 -0
- rtide_optimizer-1.0.0/python/rtide_optimizer.egg-info/SOURCES.txt +11 -0
- rtide_optimizer-1.0.0/python/rtide_optimizer.egg-info/dependency_links.txt +1 -0
- rtide_optimizer-1.0.0/python/rtide_optimizer.egg-info/requires.txt +1 -0
- rtide_optimizer-1.0.0/python/rtide_optimizer.egg-info/top_level.txt +1 -0
- rtide_optimizer-1.0.0/setup.cfg +4 -0
- rtide_optimizer-1.0.0/tests/test_rtide.py +173 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emine Ayaz
|
|
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.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rtide-optimizer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: RTI-DE: Rejected-Trial Interpolation Differential Evolution for box-constrained continuous minimisation
|
|
5
|
+
Author: Emine Ayaz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/ayazemine/rti-de
|
|
8
|
+
Keywords: differential evolution,evolutionary algorithm,metaheuristic,global optimization,black-box optimization
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.17
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# RTI-DE: Rejected-Trial Interpolation Differential Evolution
|
|
20
|
+
|
|
21
|
+
RTI-DE is a differential evolution (DE) variant for the minimisation of a black-box function over a
|
|
22
|
+
box `[lb, ub]` within a fixed budget of function evaluations. Unlike classic DE, it does not discard
|
|
23
|
+
the evaluation of a rejected trial point. Late in the run (by default after 70 % of the budget), an
|
|
24
|
+
agent whose trial point was rejected evaluates the half step along the same displacement. If that
|
|
25
|
+
is rejected as well, it evaluates the minimiser of the parabola through the three values known on
|
|
26
|
+
that line (the *rejected-trial chain*). The other components come from established DE variants:
|
|
27
|
+
|
|
28
|
+
- DE/current-to-pbest/1 mutation (JADE) without the external archive, with midpoint bound repair;
|
|
29
|
+
- binomial crossover with a fixed rate CR = 0.9;
|
|
30
|
+
- step factor F ~ N(mu_F, 0.15) clipped to [0.05, 1], where mu_F is adapted from the successful
|
|
31
|
+
trial points (weighted Lehmer mean);
|
|
32
|
+
- linear population size reduction from 18 D to 4 agents (L-SHADE).
|
|
33
|
+
|
|
34
|
+
The complete rules are in the docstring of `python/RTIDE/optimizer.py` and in Section 3 of the paper.
|
|
35
|
+
|
|
36
|
+
This repository contains:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
python/RTIDE/ the Python package (import name RTIDE, distribution name rtide-optimizer)
|
|
40
|
+
matlab/ MATLAB / GNU Octave port (rti_de.m), example and verification against Python
|
|
41
|
+
tests/ unit tests of the algorithm (Python)
|
|
42
|
+
experiments/ code of the experiments of the paper (benchmarks, engineering applications,
|
|
43
|
+
experiment runner, table and figure scripts, their tests)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Publishing to PyPI
|
|
47
|
+
|
|
48
|
+
Releases are built and published from GitHub tags using PyPI Trusted Publishing. To enable the
|
|
49
|
+
first release, sign in to PyPI as `emineayaz` and add a pending publisher at
|
|
50
|
+
<https://pypi.org/manage/account/publishing/> with these values:
|
|
51
|
+
|
|
52
|
+
- PyPI project name: `rtide-optimizer`
|
|
53
|
+
- GitHub owner: `ayazemine`
|
|
54
|
+
- GitHub repository: `rti-de`
|
|
55
|
+
- Workflow: `publish.yml`
|
|
56
|
+
- GitHub environment: `pypi`
|
|
57
|
+
|
|
58
|
+
Create the `pypi` environment in the GitHub repository settings and require a reviewer before
|
|
59
|
+
publishing. The pending publisher does not reserve the project name until its first successful
|
|
60
|
+
upload.
|
|
61
|
+
|
|
62
|
+
For each release, update `version` in `pyproject.toml`, create a matching `v<version>` tag, and
|
|
63
|
+
push it to GitHub. For example, the current `1.0.0` version can be published with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git tag v1.0.0
|
|
67
|
+
git push origin v1.0.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The workflow builds and checks the distributions before publishing them to PyPI.
|
|
71
|
+
|
|
72
|
+
## Installation (Python)
|
|
73
|
+
|
|
74
|
+
The only dependency is NumPy; the code was tested with Python 3.11.9 and NumPy 1.26.0.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install rtide-optimizer # from PyPI
|
|
78
|
+
pip install . # from a clone of this repository
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Usage (Python)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
from RTIDE import rti_de
|
|
86
|
+
|
|
87
|
+
def rastrigin(X): # vectorised: (k, D) array -> k values
|
|
88
|
+
return 10 * X.shape[1] + np.sum(X**2 - 10 * np.cos(2 * np.pi * X), axis=1)
|
|
89
|
+
|
|
90
|
+
D = 10
|
|
91
|
+
lb, ub = np.full(D, -5.12), np.full(D, 5.12)
|
|
92
|
+
x_best, f_best, fes = rti_de(rastrigin, lb, ub, max_fes=50_000, seed=1)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Optional arguments (defaults): `n_init=None` (18 D), `n_min=4`, `p=0.11`, `chain=2`,
|
|
96
|
+
`chain_from=0.7`, `CR=0.9` (`None`: no crossover), `lpsr=True`, `adapt=True`, and `trace=None`
|
|
97
|
+
(a list that receives one record per generation). A NaN objective value is treated as +inf. The
|
|
98
|
+
function returns `(x_best, f_best, evaluations_used)`.
|
|
99
|
+
|
|
100
|
+
## Usage (MATLAB / GNU Octave)
|
|
101
|
+
|
|
102
|
+
```matlab
|
|
103
|
+
addpath('matlab')
|
|
104
|
+
rastrigin = @(X) 10 * size(X, 2) + sum(X .^ 2 - 10 * cos(2 * pi * X), 2); % one point per row
|
|
105
|
+
D = 10;
|
|
106
|
+
[x_best, f_best, fes] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1);
|
|
107
|
+
[x_best, f_best, fes, trace] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1, ...
|
|
108
|
+
'chain_from', 0.5, 'CR', 0.9);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`rti_de.m` takes the same options as name-value pairs (or a struct) and also needs `rtide_rules.m`.
|
|
112
|
+
See `matlab/example_rti_de.m`. MATLAB R2016b or later is required (implicit expansion). Because the
|
|
113
|
+
two languages use different random number generators, a MATLAB run with a given seed differs from
|
|
114
|
+
the Python run with that seed. With the same random numbers, the two implementations give
|
|
115
|
+
bit-identical runs. `matlab/VERIFICATION.md` reports this check (GNU Octave 11.3.0).
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m unittest discover -s tests -v # algorithm; also imports DE and L-SHADE from experiments/
|
|
121
|
+
cd experiments && python -W ignore -m unittest discover -s tests -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Install `experiments/requirements-experiments.txt` first. The CEC tests compile the official C
|
|
125
|
+
code with `g++` on first use (into `experiments/benchmarks/official_cec/_build/`). The reference-run
|
|
126
|
+
test in `tests/test_rtide.py` compares a stored CEC2017 result bit-exactly on Windows (MinGW build)
|
|
127
|
+
and with a relative tolerance of 1e-3 elsewhere.
|
|
128
|
+
|
|
129
|
+
MATLAB/Octave verification (from `matlab/verification/`):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python verify_rtide.py prepare
|
|
133
|
+
octave-cli --no-gui --quiet --eval "run_octave_checks"
|
|
134
|
+
python verify_rtide.py compare # writes output/summary.json and ../VERIFICATION.md
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Reproducing the experiments of the paper
|
|
138
|
+
|
|
139
|
+
All commands are run from `experiments/` in a Python 3.11 environment:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
cd experiments
|
|
143
|
+
pip install -r requirements-experiments.txt
|
|
144
|
+
python benchmarks/cec_official_functions.py # builds and checks the official CEC libraries (g++)
|
|
145
|
+
|
|
146
|
+
# CEC2022 (D = 20) and CEC2017 (D = 100): 7 algorithms, 30 runs, 1,000,000 evaluations
|
|
147
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31
|
|
148
|
+
|
|
149
|
+
# engineering problems (composite laminate, with and without contiguity constraint; UAV path planning)
|
|
150
|
+
python -W ignore run_experiments.py --suites composite composite_contiguity uav --runs 30 --workers 31
|
|
151
|
+
|
|
152
|
+
# ablation (6 variants) and parameter sensitivity (8 variants)
|
|
153
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-noChain RTI-DE-halfStep RTI-DE-chainAlways RTI-DE-noLPSR RTI-DE-fixedF RTI-DE-noCR --out results_cache/paper_ablation
|
|
154
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-cf50 RTI-DE-cf90 RTI-DE-p05 RTI-DE-p20 RTI-DE-n10D RTI-DE-n25D RTI-DE-cr70 RTI-DE-cr95 --out results_cache/paper_sensitivity
|
|
155
|
+
|
|
156
|
+
# tables and figures (written to experiments/outputs/)
|
|
157
|
+
python -W ignore scripts/make_tables.py
|
|
158
|
+
python -W ignore scripts/make_figures.py
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `--dry-run` lists the work and a rough duration without running anything.
|
|
162
|
+
- An interrupted command can be restarted; finished runs are skipped.
|
|
163
|
+
- `--workers` should be at most the number of cores minus one.
|
|
164
|
+
- The runner refuses to continue a result directory if `python/RTIDE/optimizer.py` or the
|
|
165
|
+
reference implementations in `experiments/benchmarks/` have changed (SHA-256 recorded in
|
|
166
|
+
`config.json`).
|
|
167
|
+
- `experiments/applications/composite/exhaustive_search.py` recomputes the exact reference optima of
|
|
168
|
+
the laminate problems (`reference_optimum.json`).
|
|
169
|
+
- `experiments/scripts/supplementary_stats.py` computes supplementary statistics (seed-paired Wilcoxon
|
|
170
|
+
tests, rank-sum tests with a tie tolerance, Holm adjustment over a suite) from the generated
|
|
171
|
+
tables and from the `run_log.txt` and `summary.csv` files of `run_experiments.py`, copied to
|
|
172
|
+
`experiments/results_summary/<set>/`.
|
|
173
|
+
|
|
174
|
+
The values of all tables of the paper, at full precision, are in `experiments/results_tables/`
|
|
175
|
+
(one CSV file per table; the README there maps the files to the table numbers). The raw results of
|
|
176
|
+
the individual runs are not included in this repository. The problems are described in
|
|
177
|
+
`experiments/applications/README.md` and the CEC code in
|
|
178
|
+
`experiments/benchmarks/official_cec/README.md`.
|
|
179
|
+
|
|
180
|
+
## License and citation
|
|
181
|
+
|
|
182
|
+
The RTI-DE code is released under the MIT License (`LICENSE`). If you use RTI-DE in research, please
|
|
183
|
+
cite the paper:
|
|
184
|
+
|
|
185
|
+
> E. Ayaz, "Rejected-Trial Interpolation Differential Evolution with Applications to Composite
|
|
186
|
+
> Laminate Design and UAV Path Planning."
|
|
187
|
+
|
|
188
|
+
Citation metadata: `CITATION.cff`.
|
|
189
|
+
|
|
190
|
+
The official CEC 2017/2022 test-function code in `experiments/benchmarks/official_cec/` and the SPSO
|
|
191
|
+
terrain data in `experiments/applications/uav/data/` are third-party material under their own
|
|
192
|
+
terms; see `NOTICE.md`.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# RTI-DE: Rejected-Trial Interpolation Differential Evolution
|
|
2
|
+
|
|
3
|
+
RTI-DE is a differential evolution (DE) variant for the minimisation of a black-box function over a
|
|
4
|
+
box `[lb, ub]` within a fixed budget of function evaluations. Unlike classic DE, it does not discard
|
|
5
|
+
the evaluation of a rejected trial point. Late in the run (by default after 70 % of the budget), an
|
|
6
|
+
agent whose trial point was rejected evaluates the half step along the same displacement. If that
|
|
7
|
+
is rejected as well, it evaluates the minimiser of the parabola through the three values known on
|
|
8
|
+
that line (the *rejected-trial chain*). The other components come from established DE variants:
|
|
9
|
+
|
|
10
|
+
- DE/current-to-pbest/1 mutation (JADE) without the external archive, with midpoint bound repair;
|
|
11
|
+
- binomial crossover with a fixed rate CR = 0.9;
|
|
12
|
+
- step factor F ~ N(mu_F, 0.15) clipped to [0.05, 1], where mu_F is adapted from the successful
|
|
13
|
+
trial points (weighted Lehmer mean);
|
|
14
|
+
- linear population size reduction from 18 D to 4 agents (L-SHADE).
|
|
15
|
+
|
|
16
|
+
The complete rules are in the docstring of `python/RTIDE/optimizer.py` and in Section 3 of the paper.
|
|
17
|
+
|
|
18
|
+
This repository contains:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
python/RTIDE/ the Python package (import name RTIDE, distribution name rtide-optimizer)
|
|
22
|
+
matlab/ MATLAB / GNU Octave port (rti_de.m), example and verification against Python
|
|
23
|
+
tests/ unit tests of the algorithm (Python)
|
|
24
|
+
experiments/ code of the experiments of the paper (benchmarks, engineering applications,
|
|
25
|
+
experiment runner, table and figure scripts, their tests)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Publishing to PyPI
|
|
29
|
+
|
|
30
|
+
Releases are built and published from GitHub tags using PyPI Trusted Publishing. To enable the
|
|
31
|
+
first release, sign in to PyPI as `emineayaz` and add a pending publisher at
|
|
32
|
+
<https://pypi.org/manage/account/publishing/> with these values:
|
|
33
|
+
|
|
34
|
+
- PyPI project name: `rtide-optimizer`
|
|
35
|
+
- GitHub owner: `ayazemine`
|
|
36
|
+
- GitHub repository: `rti-de`
|
|
37
|
+
- Workflow: `publish.yml`
|
|
38
|
+
- GitHub environment: `pypi`
|
|
39
|
+
|
|
40
|
+
Create the `pypi` environment in the GitHub repository settings and require a reviewer before
|
|
41
|
+
publishing. The pending publisher does not reserve the project name until its first successful
|
|
42
|
+
upload.
|
|
43
|
+
|
|
44
|
+
For each release, update `version` in `pyproject.toml`, create a matching `v<version>` tag, and
|
|
45
|
+
push it to GitHub. For example, the current `1.0.0` version can be published with:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git tag v1.0.0
|
|
49
|
+
git push origin v1.0.0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The workflow builds and checks the distributions before publishing them to PyPI.
|
|
53
|
+
|
|
54
|
+
## Installation (Python)
|
|
55
|
+
|
|
56
|
+
The only dependency is NumPy; the code was tested with Python 3.11.9 and NumPy 1.26.0.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install rtide-optimizer # from PyPI
|
|
60
|
+
pip install . # from a clone of this repository
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage (Python)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import numpy as np
|
|
67
|
+
from RTIDE import rti_de
|
|
68
|
+
|
|
69
|
+
def rastrigin(X): # vectorised: (k, D) array -> k values
|
|
70
|
+
return 10 * X.shape[1] + np.sum(X**2 - 10 * np.cos(2 * np.pi * X), axis=1)
|
|
71
|
+
|
|
72
|
+
D = 10
|
|
73
|
+
lb, ub = np.full(D, -5.12), np.full(D, 5.12)
|
|
74
|
+
x_best, f_best, fes = rti_de(rastrigin, lb, ub, max_fes=50_000, seed=1)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Optional arguments (defaults): `n_init=None` (18 D), `n_min=4`, `p=0.11`, `chain=2`,
|
|
78
|
+
`chain_from=0.7`, `CR=0.9` (`None`: no crossover), `lpsr=True`, `adapt=True`, and `trace=None`
|
|
79
|
+
(a list that receives one record per generation). A NaN objective value is treated as +inf. The
|
|
80
|
+
function returns `(x_best, f_best, evaluations_used)`.
|
|
81
|
+
|
|
82
|
+
## Usage (MATLAB / GNU Octave)
|
|
83
|
+
|
|
84
|
+
```matlab
|
|
85
|
+
addpath('matlab')
|
|
86
|
+
rastrigin = @(X) 10 * size(X, 2) + sum(X .^ 2 - 10 * cos(2 * pi * X), 2); % one point per row
|
|
87
|
+
D = 10;
|
|
88
|
+
[x_best, f_best, fes] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1);
|
|
89
|
+
[x_best, f_best, fes, trace] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1, ...
|
|
90
|
+
'chain_from', 0.5, 'CR', 0.9);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`rti_de.m` takes the same options as name-value pairs (or a struct) and also needs `rtide_rules.m`.
|
|
94
|
+
See `matlab/example_rti_de.m`. MATLAB R2016b or later is required (implicit expansion). Because the
|
|
95
|
+
two languages use different random number generators, a MATLAB run with a given seed differs from
|
|
96
|
+
the Python run with that seed. With the same random numbers, the two implementations give
|
|
97
|
+
bit-identical runs. `matlab/VERIFICATION.md` reports this check (GNU Octave 11.3.0).
|
|
98
|
+
|
|
99
|
+
## Tests
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m unittest discover -s tests -v # algorithm; also imports DE and L-SHADE from experiments/
|
|
103
|
+
cd experiments && python -W ignore -m unittest discover -s tests -v
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Install `experiments/requirements-experiments.txt` first. The CEC tests compile the official C
|
|
107
|
+
code with `g++` on first use (into `experiments/benchmarks/official_cec/_build/`). The reference-run
|
|
108
|
+
test in `tests/test_rtide.py` compares a stored CEC2017 result bit-exactly on Windows (MinGW build)
|
|
109
|
+
and with a relative tolerance of 1e-3 elsewhere.
|
|
110
|
+
|
|
111
|
+
MATLAB/Octave verification (from `matlab/verification/`):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
python verify_rtide.py prepare
|
|
115
|
+
octave-cli --no-gui --quiet --eval "run_octave_checks"
|
|
116
|
+
python verify_rtide.py compare # writes output/summary.json and ../VERIFICATION.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Reproducing the experiments of the paper
|
|
120
|
+
|
|
121
|
+
All commands are run from `experiments/` in a Python 3.11 environment:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd experiments
|
|
125
|
+
pip install -r requirements-experiments.txt
|
|
126
|
+
python benchmarks/cec_official_functions.py # builds and checks the official CEC libraries (g++)
|
|
127
|
+
|
|
128
|
+
# CEC2022 (D = 20) and CEC2017 (D = 100): 7 algorithms, 30 runs, 1,000,000 evaluations
|
|
129
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31
|
|
130
|
+
|
|
131
|
+
# engineering problems (composite laminate, with and without contiguity constraint; UAV path planning)
|
|
132
|
+
python -W ignore run_experiments.py --suites composite composite_contiguity uav --runs 30 --workers 31
|
|
133
|
+
|
|
134
|
+
# ablation (6 variants) and parameter sensitivity (8 variants)
|
|
135
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-noChain RTI-DE-halfStep RTI-DE-chainAlways RTI-DE-noLPSR RTI-DE-fixedF RTI-DE-noCR --out results_cache/paper_ablation
|
|
136
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-cf50 RTI-DE-cf90 RTI-DE-p05 RTI-DE-p20 RTI-DE-n10D RTI-DE-n25D RTI-DE-cr70 RTI-DE-cr95 --out results_cache/paper_sensitivity
|
|
137
|
+
|
|
138
|
+
# tables and figures (written to experiments/outputs/)
|
|
139
|
+
python -W ignore scripts/make_tables.py
|
|
140
|
+
python -W ignore scripts/make_figures.py
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- `--dry-run` lists the work and a rough duration without running anything.
|
|
144
|
+
- An interrupted command can be restarted; finished runs are skipped.
|
|
145
|
+
- `--workers` should be at most the number of cores minus one.
|
|
146
|
+
- The runner refuses to continue a result directory if `python/RTIDE/optimizer.py` or the
|
|
147
|
+
reference implementations in `experiments/benchmarks/` have changed (SHA-256 recorded in
|
|
148
|
+
`config.json`).
|
|
149
|
+
- `experiments/applications/composite/exhaustive_search.py` recomputes the exact reference optima of
|
|
150
|
+
the laminate problems (`reference_optimum.json`).
|
|
151
|
+
- `experiments/scripts/supplementary_stats.py` computes supplementary statistics (seed-paired Wilcoxon
|
|
152
|
+
tests, rank-sum tests with a tie tolerance, Holm adjustment over a suite) from the generated
|
|
153
|
+
tables and from the `run_log.txt` and `summary.csv` files of `run_experiments.py`, copied to
|
|
154
|
+
`experiments/results_summary/<set>/`.
|
|
155
|
+
|
|
156
|
+
The values of all tables of the paper, at full precision, are in `experiments/results_tables/`
|
|
157
|
+
(one CSV file per table; the README there maps the files to the table numbers). The raw results of
|
|
158
|
+
the individual runs are not included in this repository. The problems are described in
|
|
159
|
+
`experiments/applications/README.md` and the CEC code in
|
|
160
|
+
`experiments/benchmarks/official_cec/README.md`.
|
|
161
|
+
|
|
162
|
+
## License and citation
|
|
163
|
+
|
|
164
|
+
The RTI-DE code is released under the MIT License (`LICENSE`). If you use RTI-DE in research, please
|
|
165
|
+
cite the paper:
|
|
166
|
+
|
|
167
|
+
> E. Ayaz, "Rejected-Trial Interpolation Differential Evolution with Applications to Composite
|
|
168
|
+
> Laminate Design and UAV Path Planning."
|
|
169
|
+
|
|
170
|
+
Citation metadata: `CITATION.cff`.
|
|
171
|
+
|
|
172
|
+
The official CEC 2017/2022 test-function code in `experiments/benchmarks/official_cec/` and the SPSO
|
|
173
|
+
terrain data in `experiments/applications/uav/data/` are third-party material under their own
|
|
174
|
+
terms; see `NOTICE.md`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rtide-optimizer"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "RTI-DE: Rejected-Trial Interpolation Differential Evolution for box-constrained continuous minimisation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{ name = "Emine Ayaz" }]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = ["numpy>=1.17"]
|
|
15
|
+
keywords = ["differential evolution", "evolutionary algorithm", "metaheuristic", "global optimization", "black-box optimization"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/ayazemine/rti-de"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = { "" = "python" }
|
|
28
|
+
packages = ["RTIDE"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI-DE: Rejected-Trial Interpolation Differential Evolution.
|
|
3
|
+
|
|
4
|
+
A differential evolution variant in which the evaluation of a rejected trial point is used
|
|
5
|
+
instead of being discarded.
|
|
6
|
+
|
|
7
|
+
Components
|
|
8
|
+
----------
|
|
9
|
+
1. Trial point
|
|
10
|
+
v = x + F (x_e - x) + F (x_r1 - x_r2),
|
|
11
|
+
x_e drawn from the best p N agents, r1 != r2 != i: the DE/current-to-pbest/1 mutation of JADE
|
|
12
|
+
(Zhang and Sanderson, 2009) without the external archive, followed by the binomial crossover
|
|
13
|
+
of classic DE (Storn and Price, 1997) with the fixed rate CR: component j of the trial point
|
|
14
|
+
is v_j with probability CR and for one random j, and x_j otherwise. The displacement is
|
|
15
|
+
d = trial point - x.
|
|
16
|
+
2. Selection: v replaces x when f(v) <= f(x).
|
|
17
|
+
3. Rejected-trial chain, used after the fraction ``chain_from`` of the evaluation budget.
|
|
18
|
+
An agent whose trial point was rejected does not draw a new displacement at once:
|
|
19
|
+
second point x + d / 2
|
|
20
|
+
third point x + t* d, t* the minimiser of the parabola through the three collinear
|
|
21
|
+
points x, x + d / 2, x + d, limited to [-1, 1]; t* = -1/2 when the parabola
|
|
22
|
+
is not convex. The point is skipped when t* is closer than 0.02 to a point
|
|
23
|
+
already evaluated (0, 1/2 or 1).
|
|
24
|
+
An accepted point ends the chain; after the third point a new displacement is drawn.
|
|
25
|
+
4. Step factor: F ~ N(mu_F, 0.15) clipped to [0.05, 1], mu_F = 0.5 at the start. After a
|
|
26
|
+
generation in which first trial points were accepted,
|
|
27
|
+
mu_F <- 0.9 mu_F + 0.1 L, L = sum(w F^2) / sum(w F),
|
|
28
|
+
over the accepted first trial points, w proportional to the improvement (the adaptation rule
|
|
29
|
+
of JADE with the improvement weights of SHADE; points of the chain do not enter).
|
|
30
|
+
5. Population size: reduced linearly in the evaluations used from n_init to n_min, the worst
|
|
31
|
+
agents being removed (linear population size reduction of L-SHADE, Tanabe and Fukunaga, 2014).
|
|
32
|
+
|
|
33
|
+
Bounds: a component of v outside the box is set to the midpoint between the bound and x (before
|
|
34
|
+
the crossover), and d is the displacement actually made; the third point is clipped to the box.
|
|
35
|
+
|
|
36
|
+
CR = 0.9 was chosen on a development setting (CEC2017, D = 30, 300,000 evaluations, seeds 0-4,
|
|
37
|
+
among 0.5, 0.7, 0.9 and no crossover) and confirmed on seeds 5-14 on 2026-10-10.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def rti_de(func, lb, ub, max_fes, seed=None, n_init=None, n_min=4, p=0.11, chain=2, chain_from=0.7,
|
|
44
|
+
CR=0.9, lpsr=True, adapt=True, trace=None):
|
|
45
|
+
"""Minimise ``func`` (vectorised: (k, D) array -> k values) within ``max_fes`` evaluations.
|
|
46
|
+
|
|
47
|
+
n_init initial population size (default 18 D)
|
|
48
|
+
n_min final population size
|
|
49
|
+
p fraction of the population from which x_e is drawn
|
|
50
|
+
chain points of the rejected-trial chain: 2 (half step and parabola), 1 (half step
|
|
51
|
+
only) or 0 (no chain); values below 2 are for the ablation
|
|
52
|
+
chain_from fraction of the budget after which the chain is used
|
|
53
|
+
CR rate of the binomial crossover (None: no crossover; for the ablation)
|
|
54
|
+
|
|
55
|
+
Switches for the ablation (the algorithm is the default):
|
|
56
|
+
lpsr False: the population keeps its initial size
|
|
57
|
+
adapt False: mu_F stays at 0.5
|
|
58
|
+
|
|
59
|
+
trace None, or a list to which one dict is appended per generation (the run itself is
|
|
60
|
+
not changed: no random number is drawn and nothing is evaluated for the trace):
|
|
61
|
+
fes, n, mu_F, f_best evaluations used, population size, mean step factor and
|
|
62
|
+
best value after the generation (n before the reduction)
|
|
63
|
+
evaluated, accepted, candidates of the generation by stage [first trial point,
|
|
64
|
+
improved half step, parabola point]: evaluated, accepted
|
|
65
|
+
(f <= f of the agent), strictly better
|
|
66
|
+
skipped third points left out by the skip rule
|
|
67
|
+
theta, theta_accepted factor t* of every third point evaluated and whether it
|
|
68
|
+
was accepted
|
|
69
|
+
The list is filled as the run goes, so it is valid when ``func`` ends the run early.
|
|
70
|
+
|
|
71
|
+
Returns (x_best, f_best, evaluations used)."""
|
|
72
|
+
rng = np.random.default_rng(seed)
|
|
73
|
+
lb, ub = np.asarray(lb, float), np.asarray(ub, float)
|
|
74
|
+
dim = lb.size
|
|
75
|
+
n_init = int(n_init or 18 * dim)
|
|
76
|
+
n = n_init
|
|
77
|
+
|
|
78
|
+
def evaluate(X):
|
|
79
|
+
f = np.asarray(func(X), dtype=float)
|
|
80
|
+
return np.where(np.isnan(f), np.inf, f)
|
|
81
|
+
|
|
82
|
+
X = rng.uniform(lb, ub, (n, dim))
|
|
83
|
+
fit = evaluate(X)
|
|
84
|
+
fes = n
|
|
85
|
+
mu_F = 0.5
|
|
86
|
+
stage = np.zeros(n, dtype=int) # 0: first trial point, 1: half step, 2: parabola
|
|
87
|
+
D = np.zeros((n, dim)) # displacement of the running chain
|
|
88
|
+
T = np.ones(n) # factor of d in the current candidate
|
|
89
|
+
F = np.full(n, 0.5) # step factor with which d was drawn
|
|
90
|
+
f_one, f_half = np.zeros(n), np.zeros(n) # values at x + d and x + d / 2
|
|
91
|
+
|
|
92
|
+
while fes < max_fes:
|
|
93
|
+
# --- third point: minimiser of the parabola through (0, fit), (1/2, f_half), (1, f_one)
|
|
94
|
+
s2 = np.nonzero(stage == 2)[0]
|
|
95
|
+
n_skip = 0
|
|
96
|
+
if s2.size:
|
|
97
|
+
d_one, d_half = f_one[s2] - fit[s2], f_half[s2] - fit[s2]
|
|
98
|
+
with np.errstate(all="ignore"):
|
|
99
|
+
beta = (d_half - 0.5 * d_one) / -0.25
|
|
100
|
+
t = -(d_one - beta) / (2.0 * beta)
|
|
101
|
+
t = np.where(np.isfinite(t) & (beta > 0), t, -0.5)
|
|
102
|
+
ok = np.isfinite(d_one) & np.isfinite(d_half)
|
|
103
|
+
t = np.clip(np.where(ok, t, 0.0), -1.0, 1.0)
|
|
104
|
+
ok &= (np.abs(t) >= 0.02) & (np.abs(t - 0.5) >= 0.02) & (np.abs(t - 1.0) >= 0.02)
|
|
105
|
+
T[s2[ok]] = t[ok]
|
|
106
|
+
stage[s2[~ok]] = 0
|
|
107
|
+
n_skip = int((~ok).sum())
|
|
108
|
+
|
|
109
|
+
# --- second point: half step
|
|
110
|
+
T[stage == 1] = 0.5
|
|
111
|
+
|
|
112
|
+
# --- first trial point: new displacement
|
|
113
|
+
s0 = np.nonzero(stage == 0)[0]
|
|
114
|
+
if s0.size:
|
|
115
|
+
k = s0.size
|
|
116
|
+
order = np.argsort(fit, kind="stable")
|
|
117
|
+
elite = X[order[rng.integers(max(2, int(round(p * n))), size=k)]]
|
|
118
|
+
f = np.clip(rng.normal(mu_F, 0.15, k), 0.05, 1.0)
|
|
119
|
+
r1 = rng.integers(n - 1, size=k)
|
|
120
|
+
r1 += r1 >= s0
|
|
121
|
+
r2 = rng.integers(n, size=k)
|
|
122
|
+
while True:
|
|
123
|
+
bad = (r2 == s0) | (r2 == r1)
|
|
124
|
+
if not bad.any():
|
|
125
|
+
break
|
|
126
|
+
r2[bad] = rng.integers(n, size=int(bad.sum()))
|
|
127
|
+
V = X[s0] + f[:, None] * (elite - X[s0]) + f[:, None] * (X[r1] - X[r2])
|
|
128
|
+
V = np.where(V < lb, (lb + X[s0]) / 2.0, V)
|
|
129
|
+
V = np.where(V > ub, (ub + X[s0]) / 2.0, V)
|
|
130
|
+
if CR is not None:
|
|
131
|
+
cross = rng.random((k, dim)) < CR
|
|
132
|
+
cross[np.arange(k), rng.integers(dim, size=k)] = True
|
|
133
|
+
V = np.where(cross, V, X[s0])
|
|
134
|
+
D[s0], F[s0], T[s0] = V - X[s0], f, 1.0
|
|
135
|
+
|
|
136
|
+
U = np.clip(X + T[:, None] * D, lb, ub)
|
|
137
|
+
k = min(n, max_fes - fes) # the last generation is cut at the budget
|
|
138
|
+
f_new = np.full(n, np.inf)
|
|
139
|
+
f_new[:k] = evaluate(U[:k])
|
|
140
|
+
fes += k
|
|
141
|
+
|
|
142
|
+
better = f_new < fit
|
|
143
|
+
accept = f_new <= fit
|
|
144
|
+
accept[k:] = False
|
|
145
|
+
|
|
146
|
+
# --- step factor: accepted first trial points only
|
|
147
|
+
counted = better & (stage == 0)
|
|
148
|
+
if adapt and counted.any():
|
|
149
|
+
gain = fit[counted] - f_new[counted]
|
|
150
|
+
gain = np.where(np.isfinite(gain), gain, 0.0)
|
|
151
|
+
w = gain / gain.sum() if gain.sum() > 0 else np.full(gain.size, 1.0 / gain.size)
|
|
152
|
+
s_f = F[counted] * np.abs(T[counted])
|
|
153
|
+
den = np.sum(w * s_f)
|
|
154
|
+
lehmer = np.sum(w * s_f ** 2) / den if den > 0 else 0.0
|
|
155
|
+
mu_F = float(np.clip(0.9 * mu_F + 0.1 * lehmer, 0.05, 1.0))
|
|
156
|
+
|
|
157
|
+
if trace is not None:
|
|
158
|
+
done = np.arange(n) < k
|
|
159
|
+
by_stage = [done & (stage == s) for s in (0, 1, 2)]
|
|
160
|
+
trace.append(dict(
|
|
161
|
+
fes=fes, n=n, mu_F=mu_F, f_best=float(min(fit.min(), f_new[:k].min())),
|
|
162
|
+
evaluated=[int(m.sum()) for m in by_stage],
|
|
163
|
+
accepted=[int((m & accept).sum()) for m in by_stage],
|
|
164
|
+
improved=[int((m & better).sum()) for m in by_stage],
|
|
165
|
+
skipped=n_skip, theta=T[by_stage[2]].copy(), theta_accepted=accept[by_stage[2]].copy()))
|
|
166
|
+
|
|
167
|
+
# --- selection and next stage of the chain
|
|
168
|
+
rejected = ~accept
|
|
169
|
+
rejected[k:] = False
|
|
170
|
+
f_one = np.where(rejected & (stage == 0), f_new, f_one)
|
|
171
|
+
f_half = np.where(rejected & (stage == 1), f_new, f_half)
|
|
172
|
+
X[accept], fit[accept] = U[accept], f_new[accept]
|
|
173
|
+
nxt = np.where(rejected, stage + 1, 0)
|
|
174
|
+
nxt[nxt > chain] = 0
|
|
175
|
+
if fes < chain_from * max_fes:
|
|
176
|
+
nxt[:] = 0
|
|
177
|
+
nxt[k:] = stage[k:]
|
|
178
|
+
stage = nxt
|
|
179
|
+
|
|
180
|
+
# --- linear population size reduction
|
|
181
|
+
n_next = max(n_min, int(round((n_min - n_init) / max_fes * fes + n_init))) if lpsr else n
|
|
182
|
+
if n_next < n:
|
|
183
|
+
keep = np.argsort(fit, kind="stable")[:n_next]
|
|
184
|
+
X, fit, stage, D, T, F = X[keep], fit[keep], stage[keep], D[keep], T[keep], F[keep]
|
|
185
|
+
f_one, f_half = f_one[keep], f_half[keep]
|
|
186
|
+
n = n_next
|
|
187
|
+
|
|
188
|
+
best = int(np.argmin(fit))
|
|
189
|
+
return X[best].copy(), float(fit[best]), fes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rtide-optimizer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: RTI-DE: Rejected-Trial Interpolation Differential Evolution for box-constrained continuous minimisation
|
|
5
|
+
Author: Emine Ayaz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/ayazemine/rti-de
|
|
8
|
+
Keywords: differential evolution,evolutionary algorithm,metaheuristic,global optimization,black-box optimization
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.17
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# RTI-DE: Rejected-Trial Interpolation Differential Evolution
|
|
20
|
+
|
|
21
|
+
RTI-DE is a differential evolution (DE) variant for the minimisation of a black-box function over a
|
|
22
|
+
box `[lb, ub]` within a fixed budget of function evaluations. Unlike classic DE, it does not discard
|
|
23
|
+
the evaluation of a rejected trial point. Late in the run (by default after 70 % of the budget), an
|
|
24
|
+
agent whose trial point was rejected evaluates the half step along the same displacement. If that
|
|
25
|
+
is rejected as well, it evaluates the minimiser of the parabola through the three values known on
|
|
26
|
+
that line (the *rejected-trial chain*). The other components come from established DE variants:
|
|
27
|
+
|
|
28
|
+
- DE/current-to-pbest/1 mutation (JADE) without the external archive, with midpoint bound repair;
|
|
29
|
+
- binomial crossover with a fixed rate CR = 0.9;
|
|
30
|
+
- step factor F ~ N(mu_F, 0.15) clipped to [0.05, 1], where mu_F is adapted from the successful
|
|
31
|
+
trial points (weighted Lehmer mean);
|
|
32
|
+
- linear population size reduction from 18 D to 4 agents (L-SHADE).
|
|
33
|
+
|
|
34
|
+
The complete rules are in the docstring of `python/RTIDE/optimizer.py` and in Section 3 of the paper.
|
|
35
|
+
|
|
36
|
+
This repository contains:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
python/RTIDE/ the Python package (import name RTIDE, distribution name rtide-optimizer)
|
|
40
|
+
matlab/ MATLAB / GNU Octave port (rti_de.m), example and verification against Python
|
|
41
|
+
tests/ unit tests of the algorithm (Python)
|
|
42
|
+
experiments/ code of the experiments of the paper (benchmarks, engineering applications,
|
|
43
|
+
experiment runner, table and figure scripts, their tests)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Publishing to PyPI
|
|
47
|
+
|
|
48
|
+
Releases are built and published from GitHub tags using PyPI Trusted Publishing. To enable the
|
|
49
|
+
first release, sign in to PyPI as `emineayaz` and add a pending publisher at
|
|
50
|
+
<https://pypi.org/manage/account/publishing/> with these values:
|
|
51
|
+
|
|
52
|
+
- PyPI project name: `rtide-optimizer`
|
|
53
|
+
- GitHub owner: `ayazemine`
|
|
54
|
+
- GitHub repository: `rti-de`
|
|
55
|
+
- Workflow: `publish.yml`
|
|
56
|
+
- GitHub environment: `pypi`
|
|
57
|
+
|
|
58
|
+
Create the `pypi` environment in the GitHub repository settings and require a reviewer before
|
|
59
|
+
publishing. The pending publisher does not reserve the project name until its first successful
|
|
60
|
+
upload.
|
|
61
|
+
|
|
62
|
+
For each release, update `version` in `pyproject.toml`, create a matching `v<version>` tag, and
|
|
63
|
+
push it to GitHub. For example, the current `1.0.0` version can be published with:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git tag v1.0.0
|
|
67
|
+
git push origin v1.0.0
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The workflow builds and checks the distributions before publishing them to PyPI.
|
|
71
|
+
|
|
72
|
+
## Installation (Python)
|
|
73
|
+
|
|
74
|
+
The only dependency is NumPy; the code was tested with Python 3.11.9 and NumPy 1.26.0.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install rtide-optimizer # from PyPI
|
|
78
|
+
pip install . # from a clone of this repository
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Usage (Python)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
from RTIDE import rti_de
|
|
86
|
+
|
|
87
|
+
def rastrigin(X): # vectorised: (k, D) array -> k values
|
|
88
|
+
return 10 * X.shape[1] + np.sum(X**2 - 10 * np.cos(2 * np.pi * X), axis=1)
|
|
89
|
+
|
|
90
|
+
D = 10
|
|
91
|
+
lb, ub = np.full(D, -5.12), np.full(D, 5.12)
|
|
92
|
+
x_best, f_best, fes = rti_de(rastrigin, lb, ub, max_fes=50_000, seed=1)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Optional arguments (defaults): `n_init=None` (18 D), `n_min=4`, `p=0.11`, `chain=2`,
|
|
96
|
+
`chain_from=0.7`, `CR=0.9` (`None`: no crossover), `lpsr=True`, `adapt=True`, and `trace=None`
|
|
97
|
+
(a list that receives one record per generation). A NaN objective value is treated as +inf. The
|
|
98
|
+
function returns `(x_best, f_best, evaluations_used)`.
|
|
99
|
+
|
|
100
|
+
## Usage (MATLAB / GNU Octave)
|
|
101
|
+
|
|
102
|
+
```matlab
|
|
103
|
+
addpath('matlab')
|
|
104
|
+
rastrigin = @(X) 10 * size(X, 2) + sum(X .^ 2 - 10 * cos(2 * pi * X), 2); % one point per row
|
|
105
|
+
D = 10;
|
|
106
|
+
[x_best, f_best, fes] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1);
|
|
107
|
+
[x_best, f_best, fes, trace] = rti_de(rastrigin, -5.12 * ones(1, D), 5.12 * ones(1, D), 50000, 1, ...
|
|
108
|
+
'chain_from', 0.5, 'CR', 0.9);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`rti_de.m` takes the same options as name-value pairs (or a struct) and also needs `rtide_rules.m`.
|
|
112
|
+
See `matlab/example_rti_de.m`. MATLAB R2016b or later is required (implicit expansion). Because the
|
|
113
|
+
two languages use different random number generators, a MATLAB run with a given seed differs from
|
|
114
|
+
the Python run with that seed. With the same random numbers, the two implementations give
|
|
115
|
+
bit-identical runs. `matlab/VERIFICATION.md` reports this check (GNU Octave 11.3.0).
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m unittest discover -s tests -v # algorithm; also imports DE and L-SHADE from experiments/
|
|
121
|
+
cd experiments && python -W ignore -m unittest discover -s tests -v
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Install `experiments/requirements-experiments.txt` first. The CEC tests compile the official C
|
|
125
|
+
code with `g++` on first use (into `experiments/benchmarks/official_cec/_build/`). The reference-run
|
|
126
|
+
test in `tests/test_rtide.py` compares a stored CEC2017 result bit-exactly on Windows (MinGW build)
|
|
127
|
+
and with a relative tolerance of 1e-3 elsewhere.
|
|
128
|
+
|
|
129
|
+
MATLAB/Octave verification (from `matlab/verification/`):
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python verify_rtide.py prepare
|
|
133
|
+
octave-cli --no-gui --quiet --eval "run_octave_checks"
|
|
134
|
+
python verify_rtide.py compare # writes output/summary.json and ../VERIFICATION.md
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Reproducing the experiments of the paper
|
|
138
|
+
|
|
139
|
+
All commands are run from `experiments/` in a Python 3.11 environment:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
cd experiments
|
|
143
|
+
pip install -r requirements-experiments.txt
|
|
144
|
+
python benchmarks/cec_official_functions.py # builds and checks the official CEC libraries (g++)
|
|
145
|
+
|
|
146
|
+
# CEC2022 (D = 20) and CEC2017 (D = 100): 7 algorithms, 30 runs, 1,000,000 evaluations
|
|
147
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31
|
|
148
|
+
|
|
149
|
+
# engineering problems (composite laminate, with and without contiguity constraint; UAV path planning)
|
|
150
|
+
python -W ignore run_experiments.py --suites composite composite_contiguity uav --runs 30 --workers 31
|
|
151
|
+
|
|
152
|
+
# ablation (6 variants) and parameter sensitivity (8 variants)
|
|
153
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-noChain RTI-DE-halfStep RTI-DE-chainAlways RTI-DE-noLPSR RTI-DE-fixedF RTI-DE-noCR --out results_cache/paper_ablation
|
|
154
|
+
python -W ignore run_experiments.py --suites cec2022 cec2017 --budget fe --max-fes 1000000 --runs 30 --workers 31 --algorithms RTI-DE-cf50 RTI-DE-cf90 RTI-DE-p05 RTI-DE-p20 RTI-DE-n10D RTI-DE-n25D RTI-DE-cr70 RTI-DE-cr95 --out results_cache/paper_sensitivity
|
|
155
|
+
|
|
156
|
+
# tables and figures (written to experiments/outputs/)
|
|
157
|
+
python -W ignore scripts/make_tables.py
|
|
158
|
+
python -W ignore scripts/make_figures.py
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
- `--dry-run` lists the work and a rough duration without running anything.
|
|
162
|
+
- An interrupted command can be restarted; finished runs are skipped.
|
|
163
|
+
- `--workers` should be at most the number of cores minus one.
|
|
164
|
+
- The runner refuses to continue a result directory if `python/RTIDE/optimizer.py` or the
|
|
165
|
+
reference implementations in `experiments/benchmarks/` have changed (SHA-256 recorded in
|
|
166
|
+
`config.json`).
|
|
167
|
+
- `experiments/applications/composite/exhaustive_search.py` recomputes the exact reference optima of
|
|
168
|
+
the laminate problems (`reference_optimum.json`).
|
|
169
|
+
- `experiments/scripts/supplementary_stats.py` computes supplementary statistics (seed-paired Wilcoxon
|
|
170
|
+
tests, rank-sum tests with a tie tolerance, Holm adjustment over a suite) from the generated
|
|
171
|
+
tables and from the `run_log.txt` and `summary.csv` files of `run_experiments.py`, copied to
|
|
172
|
+
`experiments/results_summary/<set>/`.
|
|
173
|
+
|
|
174
|
+
The values of all tables of the paper, at full precision, are in `experiments/results_tables/`
|
|
175
|
+
(one CSV file per table; the README there maps the files to the table numbers). The raw results of
|
|
176
|
+
the individual runs are not included in this repository. The problems are described in
|
|
177
|
+
`experiments/applications/README.md` and the CEC code in
|
|
178
|
+
`experiments/benchmarks/official_cec/README.md`.
|
|
179
|
+
|
|
180
|
+
## License and citation
|
|
181
|
+
|
|
182
|
+
The RTI-DE code is released under the MIT License (`LICENSE`). If you use RTI-DE in research, please
|
|
183
|
+
cite the paper:
|
|
184
|
+
|
|
185
|
+
> E. Ayaz, "Rejected-Trial Interpolation Differential Evolution with Applications to Composite
|
|
186
|
+
> Laminate Design and UAV Path Planning."
|
|
187
|
+
|
|
188
|
+
Citation metadata: `CITATION.cff`.
|
|
189
|
+
|
|
190
|
+
The official CEC 2017/2022 test-function code in `experiments/benchmarks/official_cec/` and the SPSO
|
|
191
|
+
terrain data in `experiments/applications/uav/data/` are third-party material under their own
|
|
192
|
+
terms; see `NOTICE.md`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
python/RTIDE/__init__.py
|
|
5
|
+
python/RTIDE/optimizer.py
|
|
6
|
+
python/rtide_optimizer.egg-info/PKG-INFO
|
|
7
|
+
python/rtide_optimizer.egg-info/SOURCES.txt
|
|
8
|
+
python/rtide_optimizer.egg-info/dependency_links.txt
|
|
9
|
+
python/rtide_optimizer.egg-info/requires.txt
|
|
10
|
+
python/rtide_optimizer.egg-info/top_level.txt
|
|
11
|
+
tests/test_rtide.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
numpy>=1.17
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
RTIDE
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Tests for python/RTIDE/optimizer.py and the reference implementations of DE and L-SHADE."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
9
|
+
ROOT = os.path.join(REPO_ROOT, "experiments") # run_experiments.py, benchmarks/
|
|
10
|
+
for _p in (os.path.join(REPO_ROOT, "python"), ROOT, os.path.join(ROOT, "benchmarks")):
|
|
11
|
+
if _p not in sys.path:
|
|
12
|
+
sys.path.insert(0, _p)
|
|
13
|
+
|
|
14
|
+
from RTIDE import rti_de # noqa: E402
|
|
15
|
+
from de_reference import de # noqa: E402
|
|
16
|
+
from lshade_reference import lshade # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _rastrigin(X):
|
|
20
|
+
return 10 * X.shape[1] + np.sum(X**2 - 10 * np.cos(2 * np.pi * X), axis=1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _shifted_sphere(X):
|
|
24
|
+
return 500.0 + np.sum((X - 1.5) ** 2, axis=1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Counted:
|
|
28
|
+
def __init__(self, f):
|
|
29
|
+
self.f, self.n, self.X = f, 0, []
|
|
30
|
+
|
|
31
|
+
def __call__(self, X):
|
|
32
|
+
self.n += X.shape[0]
|
|
33
|
+
self.X.append(np.array(X))
|
|
34
|
+
return self.f(X)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
ALGOS = {"rti_de": rti_de, "de": de, "lshade": lshade}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TestBudgetAndBounds(unittest.TestCase):
|
|
41
|
+
def test_exact_budget_and_points_inside_the_box(self):
|
|
42
|
+
lb, ub = np.full(8, -5.0), np.full(8, 5.0)
|
|
43
|
+
for name, fn in ALGOS.items():
|
|
44
|
+
for max_fes in (1000, 2357):
|
|
45
|
+
with self.subTest(algo=name, max_fes=max_fes):
|
|
46
|
+
c = Counted(_rastrigin)
|
|
47
|
+
x, f, fes = fn(c, lb, ub, max_fes, seed=0)
|
|
48
|
+
self.assertEqual((c.n, fes), (max_fes, max_fes))
|
|
49
|
+
X = np.vstack(c.X)
|
|
50
|
+
self.assertTrue(np.all(X >= lb) and np.all(X <= ub))
|
|
51
|
+
self.assertEqual(f, float(_rastrigin(x[None])[0]))
|
|
52
|
+
self.assertEqual(f, min(float(_rastrigin(b).min()) for b in c.X))
|
|
53
|
+
|
|
54
|
+
def test_same_seed_same_run(self):
|
|
55
|
+
lb, ub = np.full(6, -5.0), np.full(6, 5.0)
|
|
56
|
+
for name, fn in ALGOS.items():
|
|
57
|
+
with self.subTest(algo=name):
|
|
58
|
+
a, b = fn(_rastrigin, lb, ub, 3000, seed=3), fn(_rastrigin, lb, ub, 3000, seed=3)
|
|
59
|
+
self.assertTrue(np.array_equal(a[0], b[0]) and a[1] == b[1])
|
|
60
|
+
self.assertFalse(np.array_equal(a[0], fn(_rastrigin, lb, ub, 3000, seed=4)[0]))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestRTIDE(unittest.TestCase):
|
|
64
|
+
lb, ub = np.full(10, -5.0), np.full(10, 5.0)
|
|
65
|
+
|
|
66
|
+
def test_converges_on_a_sphere(self):
|
|
67
|
+
_, f, _ = rti_de(_shifted_sphere, self.lb, self.ub, 20000, seed=0)
|
|
68
|
+
self.assertLess(f - 500.0, 1e-6)
|
|
69
|
+
|
|
70
|
+
def test_chain_is_inactive_before_chain_from(self):
|
|
71
|
+
"""With chain_from = 1 the chain is never used: the run equals the run without chain."""
|
|
72
|
+
a = rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1, chain_from=1.0)
|
|
73
|
+
b = rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1, chain=0)
|
|
74
|
+
self.assertTrue(np.array_equal(a[0], b[0]) and a[1] == b[1])
|
|
75
|
+
|
|
76
|
+
def test_chain_changes_the_run(self):
|
|
77
|
+
full = rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1)
|
|
78
|
+
for kw in (dict(chain=0), dict(chain=1), dict(chain_from=0.0)):
|
|
79
|
+
with self.subTest(**kw):
|
|
80
|
+
self.assertFalse(np.array_equal(full[0], rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1, **kw)[0]))
|
|
81
|
+
|
|
82
|
+
def test_switches_change_the_run(self):
|
|
83
|
+
full = rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1)
|
|
84
|
+
for kw in (dict(lpsr=False), dict(adapt=False), dict(CR=None), dict(CR=0.5)):
|
|
85
|
+
with self.subTest(**kw):
|
|
86
|
+
self.assertFalse(np.array_equal(full[0], rti_de(_rastrigin, self.lb, self.ub, 6000, seed=1, **kw)[0]))
|
|
87
|
+
|
|
88
|
+
def test_constant_population_without_lpsr(self):
|
|
89
|
+
c = Counted(_rastrigin)
|
|
90
|
+
rti_de(c, self.lb, self.ub, 3000, seed=0, n_init=40, lpsr=False)
|
|
91
|
+
self.assertEqual({b.shape[0] for b in c.X[:-1]}, {40})
|
|
92
|
+
|
|
93
|
+
def test_reference_run(self):
|
|
94
|
+
"""The default algorithm must not change: stored results of two reference runs
|
|
95
|
+
(CEC2017 F12, D = 30, 300,000 evaluations, seed 8), with CR = 0.9 and without crossover.
|
|
96
|
+
|
|
97
|
+
The values were stored on Windows (MinGW build of the CEC library) and are bit-exact only
|
|
98
|
+
there: the C math library of another platform rounds differently in the last bit, and
|
|
99
|
+
the run amplifies that. Noise of one ulp on the objective values moved the two results
|
|
100
|
+
within [2203.875, 2203.897] and [2712.22, 2712.35]; elsewhere the tolerance is 1e-3 relative."""
|
|
101
|
+
try:
|
|
102
|
+
sys.path.insert(0, ROOT)
|
|
103
|
+
import run_experiments as rx
|
|
104
|
+
obj = rx.load_objective("cec2017", "F12", 30)
|
|
105
|
+
except Exception as e: # CEC library not built
|
|
106
|
+
self.skipTest(f"CEC2017 functions not available: {e!r}")
|
|
107
|
+
for kw, ref in ((dict(), 2203.883115060296), (dict(CR=None), 2712.3308479876405)):
|
|
108
|
+
with self.subTest(**kw):
|
|
109
|
+
f = rti_de(obj["vec"], obj["lb"], obj["ub"], 300000, seed=8, **kw)[1]
|
|
110
|
+
if os.name == "nt":
|
|
111
|
+
self.assertEqual(f, ref)
|
|
112
|
+
else:
|
|
113
|
+
self.assertAlmostEqual(f, ref, delta=1e-3 * ref)
|
|
114
|
+
|
|
115
|
+
def test_trace_does_not_change_the_run_and_is_consistent(self):
|
|
116
|
+
max_fes, n_init = 6000, 180
|
|
117
|
+
plain = rti_de(_rastrigin, self.lb, self.ub, max_fes, seed=1)
|
|
118
|
+
log = []
|
|
119
|
+
traced = rti_de(_rastrigin, self.lb, self.ub, max_fes, seed=1, trace=log)
|
|
120
|
+
self.assertTrue(np.array_equal(plain[0], traced[0]) and plain[1] == traced[1])
|
|
121
|
+
ev = np.array([g["evaluated"] for g in log])
|
|
122
|
+
acc = np.array([g["accepted"] for g in log])
|
|
123
|
+
imp = np.array([g["improved"] for g in log])
|
|
124
|
+
fes = np.array([g["fes"] for g in log])
|
|
125
|
+
self.assertEqual(int(ev.sum()), max_fes - n_init) # one candidate per evaluation
|
|
126
|
+
self.assertTrue(np.array_equal(np.diff(np.r_[n_init, fes]), ev.sum(axis=1)))
|
|
127
|
+
self.assertEqual(fes[-1], max_fes)
|
|
128
|
+
self.assertTrue(np.all(imp <= acc) and np.all(acc <= ev))
|
|
129
|
+
self.assertEqual(log[-1]["f_best"], plain[1])
|
|
130
|
+
self.assertTrue(np.all(np.diff([g["f_best"] for g in log]) <= 0))
|
|
131
|
+
# the chain is used only after chain_from of the budget
|
|
132
|
+
before = fes < 0.7 * max_fes
|
|
133
|
+
self.assertEqual(int(ev[before][:, 1:].sum()), 0)
|
|
134
|
+
self.assertGreater(int(ev[:, 1].sum()), 0)
|
|
135
|
+
self.assertGreater(int(ev[:, 2].sum()), 0)
|
|
136
|
+
# third points: one factor per evaluated point, all in [-1, 1/4) and not near 0
|
|
137
|
+
theta = np.concatenate([g["theta"] for g in log])
|
|
138
|
+
self.assertEqual(theta.size, int(ev[:, 2].sum()))
|
|
139
|
+
self.assertEqual(int(np.concatenate([g["theta_accepted"] for g in log]).sum()), int(acc[:, 2].sum()))
|
|
140
|
+
self.assertTrue(np.all(theta >= -1.0) and np.all(theta < 0.25) and np.all(np.abs(theta) >= 0.02))
|
|
141
|
+
# every half step that is rejected leads to a third point or to a skipped one
|
|
142
|
+
rejected_half = int((ev[:, 1] - acc[:, 1]).sum())
|
|
143
|
+
third_or_skipped = int(ev[:, 2].sum()) + sum(g["skipped"] for g in log)
|
|
144
|
+
self.assertLessEqual(third_or_skipped, rejected_half) # agents removed by the reduction are lost
|
|
145
|
+
self.assertGreaterEqual(third_or_skipped, rejected_half - n_init)
|
|
146
|
+
|
|
147
|
+
def test_trace_summary_of_the_harness(self):
|
|
148
|
+
sys.path.insert(0, ROOT)
|
|
149
|
+
import run_experiments as rx
|
|
150
|
+
max_fes, log = 6000, []
|
|
151
|
+
rti_de(_rastrigin, self.lb, self.ub, max_fes, seed=1, trace=log)
|
|
152
|
+
s = rx.summarize_rtide_trace(log, max_fes, bins=20)
|
|
153
|
+
for key in ("evaluated", "accepted", "improved"):
|
|
154
|
+
self.assertTrue(np.array_equal(s[key].sum(axis=0), np.sum([g[key] for g in log], axis=0)), key)
|
|
155
|
+
self.assertEqual(int(s["generations"].sum()), len(log))
|
|
156
|
+
self.assertEqual(int(s["skipped"].sum()), sum(g["skipped"] for g in log))
|
|
157
|
+
self.assertEqual((s["mu_F"][-1], s["n"][-1]), (log[-1]["mu_F"], log[-1]["n"]))
|
|
158
|
+
self.assertEqual(s["theta_outside"], 0)
|
|
159
|
+
self.assertEqual(int(s["theta_accepted"].sum() + s["theta_rejected"].sum()),
|
|
160
|
+
int(s["evaluated"][:, 2].sum()))
|
|
161
|
+
self.assertEqual(int(s["evaluated"][:14, 1:].sum()), 0) # parts that end before 0.7 of the budget
|
|
162
|
+
|
|
163
|
+
def test_nan_objective_is_treated_as_infeasible(self):
|
|
164
|
+
def f(X):
|
|
165
|
+
v = _rastrigin(X)
|
|
166
|
+
v[X[:, 0] > 4.0] = np.nan
|
|
167
|
+
return v
|
|
168
|
+
x, fx, _ = rti_de(f, self.lb, self.ub, 3000, seed=0)
|
|
169
|
+
self.assertTrue(np.isfinite(fx) and x[0] <= 4.0)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
unittest.main()
|