topasmoo 0.2.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.
- topasmoo-0.2.0/LICENSE +22 -0
- topasmoo-0.2.0/PKG-INFO +147 -0
- topasmoo-0.2.0/README.md +106 -0
- topasmoo-0.2.0/TopasMOO/__init__.py +81 -0
- topasmoo-0.2.0/TopasMOO/exceptions.py +23 -0
- topasmoo-0.2.0/TopasMOO/io.py +118 -0
- topasmoo-0.2.0/TopasMOO/metrics.py +141 -0
- topasmoo-0.2.0/TopasMOO/optimizers.py +1470 -0
- topasmoo-0.2.0/TopasMOO/plotting/__init__.py +57 -0
- topasmoo-0.2.0/TopasMOO/plotting/comprehensive.py +270 -0
- topasmoo-0.2.0/TopasMOO/plotting/convergence.py +221 -0
- topasmoo-0.2.0/TopasMOO/plotting/correlation.py +87 -0
- topasmoo-0.2.0/TopasMOO/plotting/decision.py +147 -0
- topasmoo-0.2.0/TopasMOO/plotting/hypervolume.py +87 -0
- topasmoo-0.2.0/TopasMOO/plotting/parallel.py +157 -0
- topasmoo-0.2.0/TopasMOO/plotting/pareto.py +413 -0
- topasmoo-0.2.0/TopasMOO/plotting/petal.py +254 -0
- topasmoo-0.2.0/TopasMOO/plotting/population.py +102 -0
- topasmoo-0.2.0/TopasMOO/plotting/style.py +337 -0
- topasmoo-0.2.0/TopasMOO/plotting/topasmoo_fast.mplstyle +47 -0
- topasmoo-0.2.0/TopasMOO/plotting/topasmoo_pub_clean.mplstyle +60 -0
- topasmoo-0.2.0/TopasMOO/plotting/topasmoo_pub_ieee.mplstyle +67 -0
- topasmoo-0.2.0/TopasMOO/plotting/topasmoo_pub_medicalphysics.mplstyle +72 -0
- topasmoo-0.2.0/TopasMOO/plotting/topasmoo_pub_nature.mplstyle +54 -0
- topasmoo-0.2.0/TopasMOO/utilities.py +86 -0
- topasmoo-0.2.0/pyproject.toml +82 -0
topasmoo-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mindy Harkness
|
|
4
|
+
(TopasOpt Copyright (c) 2022 Brendan Whelan)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
topasmoo-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: topasmoo
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Multi-objective optimization extension of TopasOpt for TOPAS Monte Carlo simulations
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: topas,monte-carlo,multi-objective-optimization,medical-physics,radiation-therapy
|
|
8
|
+
Author: Mindy Harkness
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Provides-Extra: examples
|
|
22
|
+
Provides-Extra: settings-dump
|
|
23
|
+
Requires-Dist: build (>=1.2.0) ; extra == "dev"
|
|
24
|
+
Requires-Dist: coverage (>=7.4.1) ; extra == "dev"
|
|
25
|
+
Requires-Dist: jsonpickle (>=4.0.0) ; extra == "settings-dump"
|
|
26
|
+
Requires-Dist: matplotlib (>=3.8.0)
|
|
27
|
+
Requires-Dist: numpy (>=1.26.0)
|
|
28
|
+
Requires-Dist: pymoo (>=0.6.0)
|
|
29
|
+
Requires-Dist: pytest (>=8.0.0) ; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-cov (>=4.1.0) ; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-mpl (>=0.17.0) ; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff (>=0.5.0) ; extra == "dev"
|
|
33
|
+
Requires-Dist: topas2numpy (>=0.2.0) ; extra == "examples"
|
|
34
|
+
Requires-Dist: twine (>=5.0.0) ; extra == "dev"
|
|
35
|
+
Project-URL: Changelog, https://github.com/mindyharkness/TopasMOO/blob/main/CHANGELOG.md
|
|
36
|
+
Project-URL: Homepage, https://github.com/mindyharkness/TopasMOO
|
|
37
|
+
Project-URL: Issues, https://github.com/mindyharkness/TopasMOO/issues
|
|
38
|
+
Project-URL: Repository, https://github.com/mindyharkness/TopasMOO
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# TopasMOO
|
|
42
|
+
|
|
43
|
+
[](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/topasmoo/)
|
|
45
|
+
[](https://pypi.org/project/topasmoo/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
TopasMOO is a Python toolkit for multi-objective optimization of TOPAS Monte Carlo radiation therapy simulations, enabling automated discovery of Pareto-optimal treatment designs.
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install topasmoo
|
|
54
|
+
# or from source:
|
|
55
|
+
git clone https://github.com/mindyharkness/TopasMOO.git
|
|
56
|
+
cd TopasMOO
|
|
57
|
+
pip install -e .
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For local development with [uv](https://docs.astral.sh/uv/):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv sync --extra dev
|
|
64
|
+
uv run ruff check TopasMOO tests
|
|
65
|
+
uv run pytest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Requirements**
|
|
69
|
+
|
|
70
|
+
- Python >= 3.10, < 3.13
|
|
71
|
+
- A working [TOPAS](https://topas.readthedocs.io/) installation for full Monte Carlo runs (or use `testing_mode` for development and benchmarks)
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
The optimizer expects a project directory with `GenerateTopasScripts.py` and `TopasObjectiveFunction.py`, following the TopasOpt layout. The repository’s [examples/DevelopmentExample](examples/DevelopmentExample/) folder implements the ZDT1 benchmark: TOPAS is not run, but those two files are still present so the workflow matches a real study.
|
|
76
|
+
|
|
77
|
+
From the repository root, after installing the package:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
|
|
82
|
+
import numpy as np
|
|
83
|
+
from TopasMOO import NSGAII_Optimizer
|
|
84
|
+
|
|
85
|
+
opt_dir = Path("examples/DevelopmentExample")
|
|
86
|
+
|
|
87
|
+
optimization_params = {
|
|
88
|
+
"ParameterNames": ["x1", "x2", "x3", "x4", "x5"],
|
|
89
|
+
"UpperBounds": np.ones(5),
|
|
90
|
+
"LowerBounds": np.zeros(5),
|
|
91
|
+
"start_point": np.full(5, 0.5),
|
|
92
|
+
"n_generations": 20,
|
|
93
|
+
"n_objectives": 2,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
optimizer = NSGAII_Optimizer(
|
|
97
|
+
optimization_params=optimization_params,
|
|
98
|
+
BaseDirectory=str(opt_dir),
|
|
99
|
+
SimulationName="QuickStart",
|
|
100
|
+
OptimizationDirectory=opt_dir,
|
|
101
|
+
TopasLocation="testing_mode",
|
|
102
|
+
Overwrite=True,
|
|
103
|
+
pop_size=12,
|
|
104
|
+
publication_variant="clean", # or "nature" / "ieee" / "medicalphysics"
|
|
105
|
+
)
|
|
106
|
+
results = optimizer.RunOptimization()
|
|
107
|
+
# results.X: decision variables on the Pareto set; results.F: objective values
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For a full walkthrough, plots, and validation metrics, run `python DevelopmentExample_main.py` inside `examples/DevelopmentExample/`. For collimator optimization with TOPAS, see [examples/ApertureOptimization](examples/ApertureOptimization/).
|
|
111
|
+
|
|
112
|
+
## Citation
|
|
113
|
+
|
|
114
|
+
If you use TopasMOO, please cite it (placeholder entry until a DOI is available) and the TopasOpt paper:
|
|
115
|
+
|
|
116
|
+
```bibtex
|
|
117
|
+
@software{harkness_topasmoo_2026,
|
|
118
|
+
author = {Harkness, Mindy},
|
|
119
|
+
title = {{TopasMOO}: Multi-objective optimization for {TOPAS} {Monte} {Carlo} simulations},
|
|
120
|
+
year = {2026},
|
|
121
|
+
url = {https://github.com/mindyharkness/TopasMOO},
|
|
122
|
+
note = {Placeholder: replace with published citation when available},
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@article{whelan_topasopt_2022,
|
|
126
|
+
title = {{TopasOpt}: {An} open-source library for optimization with {Topas} {Monte} {Carlo}},
|
|
127
|
+
journal = {Medical Physics},
|
|
128
|
+
author = {Whelan, Brendan and Loo Jr, Billy W. and Wang, Jinghui and Keall, Paul},
|
|
129
|
+
year = {2022},
|
|
130
|
+
publisher = {Wiley Online Library},
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
This project is released under the [MIT License](LICENSE).
|
|
137
|
+
|
|
138
|
+
## Related Projects
|
|
139
|
+
|
|
140
|
+
- [TopasOpt](https://github.com/Image-X-Institute/TopasOpt) — single-objective optimization for TOPAS
|
|
141
|
+
- [TOPAS](https://topas.readthedocs.io/) — Monte Carlo simulation for medical physics
|
|
142
|
+
- [pymoo](https://pymoo.org/) — multi-objective optimization algorithms in Python
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
TopasMOO is intended for **multi-objective** problems (at least two objectives). For a single scalar objective, use [TopasOpt](https://github.com/Image-X-Institute/TopasOpt).
|
|
147
|
+
|
topasmoo-0.2.0/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# TopasMOO
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/topasmoo/)
|
|
5
|
+
[](https://pypi.org/project/topasmoo/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
TopasMOO is a Python toolkit for multi-objective optimization of TOPAS Monte Carlo radiation therapy simulations, enabling automated discovery of Pareto-optimal treatment designs.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install topasmoo
|
|
14
|
+
# or from source:
|
|
15
|
+
git clone https://github.com/mindyharkness/TopasMOO.git
|
|
16
|
+
cd TopasMOO
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For local development with [uv](https://docs.astral.sh/uv/):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
uv sync --extra dev
|
|
24
|
+
uv run ruff check TopasMOO tests
|
|
25
|
+
uv run pytest
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Requirements**
|
|
29
|
+
|
|
30
|
+
- Python >= 3.10, < 3.13
|
|
31
|
+
- A working [TOPAS](https://topas.readthedocs.io/) installation for full Monte Carlo runs (or use `testing_mode` for development and benchmarks)
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
The optimizer expects a project directory with `GenerateTopasScripts.py` and `TopasObjectiveFunction.py`, following the TopasOpt layout. The repository’s [examples/DevelopmentExample](examples/DevelopmentExample/) folder implements the ZDT1 benchmark: TOPAS is not run, but those two files are still present so the workflow matches a real study.
|
|
36
|
+
|
|
37
|
+
From the repository root, after installing the package:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from TopasMOO import NSGAII_Optimizer
|
|
44
|
+
|
|
45
|
+
opt_dir = Path("examples/DevelopmentExample")
|
|
46
|
+
|
|
47
|
+
optimization_params = {
|
|
48
|
+
"ParameterNames": ["x1", "x2", "x3", "x4", "x5"],
|
|
49
|
+
"UpperBounds": np.ones(5),
|
|
50
|
+
"LowerBounds": np.zeros(5),
|
|
51
|
+
"start_point": np.full(5, 0.5),
|
|
52
|
+
"n_generations": 20,
|
|
53
|
+
"n_objectives": 2,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
optimizer = NSGAII_Optimizer(
|
|
57
|
+
optimization_params=optimization_params,
|
|
58
|
+
BaseDirectory=str(opt_dir),
|
|
59
|
+
SimulationName="QuickStart",
|
|
60
|
+
OptimizationDirectory=opt_dir,
|
|
61
|
+
TopasLocation="testing_mode",
|
|
62
|
+
Overwrite=True,
|
|
63
|
+
pop_size=12,
|
|
64
|
+
publication_variant="clean", # or "nature" / "ieee" / "medicalphysics"
|
|
65
|
+
)
|
|
66
|
+
results = optimizer.RunOptimization()
|
|
67
|
+
# results.X: decision variables on the Pareto set; results.F: objective values
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
For a full walkthrough, plots, and validation metrics, run `python DevelopmentExample_main.py` inside `examples/DevelopmentExample/`. For collimator optimization with TOPAS, see [examples/ApertureOptimization](examples/ApertureOptimization/).
|
|
71
|
+
|
|
72
|
+
## Citation
|
|
73
|
+
|
|
74
|
+
If you use TopasMOO, please cite it (placeholder entry until a DOI is available) and the TopasOpt paper:
|
|
75
|
+
|
|
76
|
+
```bibtex
|
|
77
|
+
@software{harkness_topasmoo_2026,
|
|
78
|
+
author = {Harkness, Mindy},
|
|
79
|
+
title = {{TopasMOO}: Multi-objective optimization for {TOPAS} {Monte} {Carlo} simulations},
|
|
80
|
+
year = {2026},
|
|
81
|
+
url = {https://github.com/mindyharkness/TopasMOO},
|
|
82
|
+
note = {Placeholder: replace with published citation when available},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@article{whelan_topasopt_2022,
|
|
86
|
+
title = {{TopasOpt}: {An} open-source library for optimization with {Topas} {Monte} {Carlo}},
|
|
87
|
+
journal = {Medical Physics},
|
|
88
|
+
author = {Whelan, Brendan and Loo Jr, Billy W. and Wang, Jinghui and Keall, Paul},
|
|
89
|
+
year = {2022},
|
|
90
|
+
publisher = {Wiley Online Library},
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
This project is released under the [MIT License](LICENSE).
|
|
97
|
+
|
|
98
|
+
## Related Projects
|
|
99
|
+
|
|
100
|
+
- [TopasOpt](https://github.com/Image-X-Institute/TopasOpt) — single-objective optimization for TOPAS
|
|
101
|
+
- [TOPAS](https://topas.readthedocs.io/) — Monte Carlo simulation for medical physics
|
|
102
|
+
- [pymoo](https://pymoo.org/) — multi-objective optimization algorithms in Python
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
TopasMOO is intended for **multi-objective** problems (at least two objectives). For a single scalar objective, use [TopasOpt](https://github.com/Image-X-Institute/TopasOpt).
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TopasMOO: Multi-Objective Optimization for TOPAS Monte Carlo Simulations
|
|
3
|
+
|
|
4
|
+
TopasMOO extends the TopasOpt framework to handle multiple competing objectives
|
|
5
|
+
using pymoo algorithms, particularly NSGA-II.
|
|
6
|
+
|
|
7
|
+
Each optimization must import:
|
|
8
|
+
``NSGAII_Optimizer``
|
|
9
|
+
The optimizer you construct and run (``.RunOptimization()``). This is the
|
|
10
|
+
main entry point for most users.
|
|
11
|
+
``TopasMOOBaseClass``
|
|
12
|
+
Base class to subclass if you want to drive a different pymoo algorithm.
|
|
13
|
+
|
|
14
|
+
The full visualization toolbox (Pareto fronts, petal diagrams, convergence plots,
|
|
15
|
+
etc.) lives in the TopasMOO.plotting subpackage; only the style entry
|
|
16
|
+
points are re-exported here for convenience. Pareto-analysis metrics live in
|
|
17
|
+
TopasMOO.metrics and log helpers in TopasMOO.io.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import importlib.metadata
|
|
21
|
+
|
|
22
|
+
__author__ = "Mindy Harkness"
|
|
23
|
+
try:
|
|
24
|
+
__version__ = importlib.metadata.version("topasmoo")
|
|
25
|
+
except importlib.metadata.PackageNotFoundError:
|
|
26
|
+
__version__ = "0.2.0-dev"
|
|
27
|
+
|
|
28
|
+
# Exceptions
|
|
29
|
+
from .exceptions import (
|
|
30
|
+
InvalidParameterError,
|
|
31
|
+
MalformedOutputError,
|
|
32
|
+
ObjectiveFunctionError,
|
|
33
|
+
TopasExecutionError,
|
|
34
|
+
TopasMOOError,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# I/O utilities
|
|
38
|
+
from .io import LogParetoFrontToFile, ReadInMultiObjectiveLogFile
|
|
39
|
+
|
|
40
|
+
# Metrics
|
|
41
|
+
from .metrics import (
|
|
42
|
+
calculate_crowding_distance,
|
|
43
|
+
calculate_dominance_rank,
|
|
44
|
+
calculate_knee_point,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Core optimizer classes (TopasProblem is an internal pymoo adapter that stays
|
|
48
|
+
# in TopasMOO.optimizers).
|
|
49
|
+
from .optimizers import NSGAII_Optimizer, TopasMOOBaseClass
|
|
50
|
+
|
|
51
|
+
# Plotting style entry points (the full plotting API lives in TopasMOO.plotting).
|
|
52
|
+
from .plotting import (
|
|
53
|
+
apply_style,
|
|
54
|
+
available_publication_variants,
|
|
55
|
+
publication_style,
|
|
56
|
+
save_publication_figure,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
# Core classes
|
|
61
|
+
"NSGAII_Optimizer",
|
|
62
|
+
"TopasMOOBaseClass",
|
|
63
|
+
# Exceptions
|
|
64
|
+
"TopasMOOError",
|
|
65
|
+
"TopasExecutionError",
|
|
66
|
+
"InvalidParameterError",
|
|
67
|
+
"ObjectiveFunctionError",
|
|
68
|
+
"MalformedOutputError",
|
|
69
|
+
# Metrics
|
|
70
|
+
"calculate_knee_point",
|
|
71
|
+
"calculate_crowding_distance",
|
|
72
|
+
"calculate_dominance_rank",
|
|
73
|
+
# IO
|
|
74
|
+
"ReadInMultiObjectiveLogFile",
|
|
75
|
+
"LogParetoFrontToFile",
|
|
76
|
+
# Plotting style entry points (full plotting API under TopasMOO.plotting)
|
|
77
|
+
"apply_style",
|
|
78
|
+
"available_publication_variants",
|
|
79
|
+
"publication_style",
|
|
80
|
+
"save_publication_figure",
|
|
81
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exception classes for TopasMOO.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TopasMOOError(Exception):
|
|
7
|
+
"""Base exception for all TopasMOO errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TopasExecutionError(TopasMOOError):
|
|
11
|
+
"""Raised when a TOPAS simulation fails to execute."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidParameterError(TopasMOOError):
|
|
15
|
+
"""Raised when optimization parameters are invalid or inconsistent."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ObjectiveFunctionError(TopasMOOError):
|
|
19
|
+
"""Raised when the objective function returns unexpected results."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MalformedOutputError(TopasMOOError):
|
|
23
|
+
"""Raised when TOPAS output files are missing or malformed."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
I/O utilities for reading and writing TopasMOO optimization logs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from collections.abc import Iterable, Sequence
|
|
10
|
+
from typing import TYPE_CHECKING, Union
|
|
11
|
+
|
|
12
|
+
from .exceptions import MalformedOutputError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
PathLike = Union[str, os.PathLike]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ReadInMultiObjectiveLogFile(LogFilePath: PathLike) -> dict[str, list[float]]:
|
|
23
|
+
"""Read a multi-objective optimization log file into a dictionary.
|
|
24
|
+
|
|
25
|
+
Parses the comma-separated log format produced by TopasMOO during
|
|
26
|
+
optimization, extracting iteration numbers, parameter values, and
|
|
27
|
+
objective function values.
|
|
28
|
+
|
|
29
|
+
:param LogFilePath: Path to the optimization log file
|
|
30
|
+
(typically ``logs/OptimizationLogs.txt``).
|
|
31
|
+
|
|
32
|
+
:returns: Dictionary mapping column names to lists of float values.
|
|
33
|
+
Keys include ``'Iteration'``, parameter names, and
|
|
34
|
+
``'ObjectiveFunction_1'``, ``'ObjectiveFunction_2'``, etc.
|
|
35
|
+
|
|
36
|
+
:raises FileNotFoundError: If the log file does not exist.
|
|
37
|
+
:raises MalformedOutputError: If a data row contains a non-numeric value
|
|
38
|
+
after the ``key: value`` split.
|
|
39
|
+
"""
|
|
40
|
+
log_path = os.fspath(LogFilePath)
|
|
41
|
+
if not os.path.isfile(log_path):
|
|
42
|
+
raise FileNotFoundError(f"Could not find log file at {log_path}")
|
|
43
|
+
|
|
44
|
+
results: dict[str, list[float]] = {}
|
|
45
|
+
with open(log_path, "r") as f:
|
|
46
|
+
lines = f.readlines()
|
|
47
|
+
|
|
48
|
+
if not lines:
|
|
49
|
+
return results
|
|
50
|
+
|
|
51
|
+
if not any("Iteration" in line for line in lines):
|
|
52
|
+
logger.warning("Log file format not recognized or empty: %s", log_path)
|
|
53
|
+
return results
|
|
54
|
+
|
|
55
|
+
for line_number, line in enumerate(lines, start=1):
|
|
56
|
+
if not line.startswith("Iteration"):
|
|
57
|
+
continue
|
|
58
|
+
for entry in line.split(","):
|
|
59
|
+
parts = entry.split(":")
|
|
60
|
+
if len(parts) < 2:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
key = parts[0].strip()
|
|
64
|
+
raw_value = parts[1].strip()
|
|
65
|
+
try:
|
|
66
|
+
value = float(raw_value)
|
|
67
|
+
except ValueError as exc:
|
|
68
|
+
raise MalformedOutputError(
|
|
69
|
+
f"Could not parse value '{raw_value}' for key '{key}' "
|
|
70
|
+
f"on line {line_number} of {log_path}"
|
|
71
|
+
) from exc
|
|
72
|
+
|
|
73
|
+
results.setdefault(key, []).append(value)
|
|
74
|
+
|
|
75
|
+
return results
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def LogParetoFrontToFile(
|
|
79
|
+
LogFilePath: PathLike,
|
|
80
|
+
ParetoObjectives: np.ndarray | Iterable[Iterable[float]],
|
|
81
|
+
ParameterNames: Sequence[str],
|
|
82
|
+
n_objectives: int,
|
|
83
|
+
ParetoDecisionVars: np.ndarray | Iterable[Iterable[float]] | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Write the current Pareto front to a CSV-style log file.
|
|
86
|
+
|
|
87
|
+
:param LogFilePath: Output file path.
|
|
88
|
+
:param ParetoObjectives: Array of shape ``(n_solutions, n_objectives)``
|
|
89
|
+
containing objective values for all non-dominated solutions.
|
|
90
|
+
:param ParameterNames: Decision-variable names, used as the column headers for
|
|
91
|
+
the decision-variable values when ``ParetoDecisionVars`` is given.
|
|
92
|
+
:param n_objectives: Number of objective functions.
|
|
93
|
+
:param ParetoDecisionVars: Optional array of shape
|
|
94
|
+
``(n_solutions, len(ParameterNames))`` aligned row-for-row with
|
|
95
|
+
``ParetoObjectives``. When provided, each parameter is written as an
|
|
96
|
+
additional column so the file fully describes each solution. When
|
|
97
|
+
``None``, only objective columns are written.
|
|
98
|
+
"""
|
|
99
|
+
with open(os.fspath(LogFilePath), "w") as f:
|
|
100
|
+
header = "Solution_Index"
|
|
101
|
+
for i in range(n_objectives):
|
|
102
|
+
header += f",Objective_{i+1}"
|
|
103
|
+
if ParetoDecisionVars is not None:
|
|
104
|
+
for name in ParameterNames:
|
|
105
|
+
header += f",{name}"
|
|
106
|
+
header += "\n"
|
|
107
|
+
f.write(header)
|
|
108
|
+
|
|
109
|
+
decision_rows = list(ParetoDecisionVars) if ParetoDecisionVars is not None else None
|
|
110
|
+
for i, objectives in enumerate(ParetoObjectives):
|
|
111
|
+
line = f"{i}"
|
|
112
|
+
for obj_val in objectives:
|
|
113
|
+
line += f",{obj_val:.6f}"
|
|
114
|
+
if decision_rows is not None:
|
|
115
|
+
for var_val in decision_rows[i]:
|
|
116
|
+
line += f",{var_val:.6f}"
|
|
117
|
+
line += "\n"
|
|
118
|
+
f.write(line)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-objective optimization metrics for Pareto front analysis.
|
|
3
|
+
|
|
4
|
+
All metrics assume minimization of objectives.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_objectives(
|
|
13
|
+
objectives: np.ndarray,
|
|
14
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
15
|
+
"""Min-max normalize objectives per column to ``[0, 1]``.
|
|
16
|
+
|
|
17
|
+
Each objective (column) is scaled by its observed range across the given
|
|
18
|
+
solutions. Columns with zero range (a single distinct value) are left at
|
|
19
|
+
``0`` rather than dividing by zero.
|
|
20
|
+
|
|
21
|
+
:param objectives: Array of shape ``(n_solutions, n_objectives)``.
|
|
22
|
+
|
|
23
|
+
:returns: Tuple ``(normalized, ideal, nadir)`` where ``ideal`` and ``nadir`` are
|
|
24
|
+
the per-objective minimum and maximum, and ``normalized`` has the same
|
|
25
|
+
shape as ``objectives``.
|
|
26
|
+
"""
|
|
27
|
+
objectives = np.asarray(objectives, dtype=float)
|
|
28
|
+
ideal = objectives.min(axis=0)
|
|
29
|
+
nadir = objectives.max(axis=0)
|
|
30
|
+
obj_range = nadir - ideal
|
|
31
|
+
obj_range[obj_range == 0] = 1
|
|
32
|
+
normalized = (objectives - ideal) / obj_range
|
|
33
|
+
return normalized, ideal, nadir
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def calculate_knee_point(pareto_objectives: np.ndarray) -> int:
|
|
37
|
+
"""Find the knee point (best trade-off) on a Pareto front.
|
|
38
|
+
|
|
39
|
+
Uses a trade-off method: the knee is the solution with the minimum
|
|
40
|
+
sum of min-max normalized objectives, representing the best balanced
|
|
41
|
+
compromise across all objectives.
|
|
42
|
+
|
|
43
|
+
:param pareto_objectives: Array of shape ``(n_solutions, n_objectives)``.
|
|
44
|
+
All objectives are assumed to be minimized.
|
|
45
|
+
|
|
46
|
+
:returns: Index (int) of the knee point solution in the input array.
|
|
47
|
+
"""
|
|
48
|
+
normalized, _ideal, _nadir = normalize_objectives(pareto_objectives)
|
|
49
|
+
|
|
50
|
+
total_objectives = normalized.sum(axis=1)
|
|
51
|
+
return int(np.argmin(total_objectives))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def calculate_crowding_distance(pareto_objectives: np.ndarray) -> np.ndarray:
|
|
55
|
+
"""Calculate NSGA-II crowding distance for each solution.
|
|
56
|
+
|
|
57
|
+
Crowding distance measures how isolated a solution is in objective
|
|
58
|
+
space. Higher values indicate solutions in less crowded regions,
|
|
59
|
+
which are preferred during selection to maintain diversity.
|
|
60
|
+
|
|
61
|
+
Boundary solutions (best/worst in any single objective) receive
|
|
62
|
+
infinite crowding distance.
|
|
63
|
+
|
|
64
|
+
:param pareto_objectives: Array of shape ``(n_solutions, n_objectives)``.
|
|
65
|
+
|
|
66
|
+
:returns: Array of crowding distances with shape ``(n_solutions,)``.
|
|
67
|
+
"""
|
|
68
|
+
n_solutions, n_objectives = pareto_objectives.shape
|
|
69
|
+
|
|
70
|
+
if n_solutions <= 2:
|
|
71
|
+
return np.full(n_solutions, np.inf)
|
|
72
|
+
|
|
73
|
+
crowding = np.zeros(n_solutions)
|
|
74
|
+
|
|
75
|
+
for m in range(n_objectives):
|
|
76
|
+
sorted_indices = np.argsort(pareto_objectives[:, m])
|
|
77
|
+
obj_values = pareto_objectives[sorted_indices, m]
|
|
78
|
+
|
|
79
|
+
crowding[sorted_indices[0]] = np.inf
|
|
80
|
+
crowding[sorted_indices[-1]] = np.inf
|
|
81
|
+
|
|
82
|
+
obj_range = obj_values[-1] - obj_values[0]
|
|
83
|
+
if obj_range == 0:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
for i in range(1, n_solutions - 1):
|
|
87
|
+
crowding[sorted_indices[i]] += (
|
|
88
|
+
obj_values[i + 1] - obj_values[i - 1]
|
|
89
|
+
) / obj_range
|
|
90
|
+
|
|
91
|
+
return crowding
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def calculate_dominance_rank(objectives: np.ndarray) -> np.ndarray:
|
|
95
|
+
"""Assign dominance ranks using fast non-dominated sorting.
|
|
96
|
+
|
|
97
|
+
Rank 0 contains the Pareto front (non-dominated solutions).
|
|
98
|
+
Rank 1 contains solutions dominated only by rank-0 solutions,
|
|
99
|
+
and so on.
|
|
100
|
+
|
|
101
|
+
All objectives are assumed to be minimized.
|
|
102
|
+
|
|
103
|
+
:param objectives: Array of shape ``(n_solutions, n_objectives)``.
|
|
104
|
+
|
|
105
|
+
:returns: Integer array of dominance ranks with shape ``(n_solutions,)``.
|
|
106
|
+
"""
|
|
107
|
+
n_solutions = len(objectives)
|
|
108
|
+
domination_count = np.zeros(n_solutions, dtype=int)
|
|
109
|
+
dominated_solutions = [[] for _ in range(n_solutions)]
|
|
110
|
+
ranks = np.zeros(n_solutions, dtype=int)
|
|
111
|
+
|
|
112
|
+
for i in range(n_solutions):
|
|
113
|
+
for j in range(i + 1, n_solutions):
|
|
114
|
+
if np.all(objectives[i] <= objectives[j]) and np.any(
|
|
115
|
+
objectives[i] < objectives[j]
|
|
116
|
+
):
|
|
117
|
+
dominated_solutions[i].append(j)
|
|
118
|
+
domination_count[j] += 1
|
|
119
|
+
elif np.all(objectives[j] <= objectives[i]) and np.any(
|
|
120
|
+
objectives[j] < objectives[i]
|
|
121
|
+
):
|
|
122
|
+
dominated_solutions[j].append(i)
|
|
123
|
+
domination_count[i] += 1
|
|
124
|
+
|
|
125
|
+
current_front = np.where(domination_count == 0)[0]
|
|
126
|
+
rank = 0
|
|
127
|
+
|
|
128
|
+
while len(current_front) > 0:
|
|
129
|
+
ranks[current_front] = rank
|
|
130
|
+
next_front = []
|
|
131
|
+
|
|
132
|
+
for i in current_front:
|
|
133
|
+
for j in dominated_solutions[i]:
|
|
134
|
+
domination_count[j] -= 1
|
|
135
|
+
if domination_count[j] == 0:
|
|
136
|
+
next_front.append(j)
|
|
137
|
+
|
|
138
|
+
current_front = np.array(next_front)
|
|
139
|
+
rank += 1
|
|
140
|
+
|
|
141
|
+
return ranks
|