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 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.
@@ -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
+ [![CI](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml/badge.svg)](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml)
44
+ [![PyPI version](https://img.shields.io/pypi/v/topasmoo.svg)](https://pypi.org/project/topasmoo/)
45
+ [![Python versions](https://img.shields.io/pypi/pyversions/topasmoo.svg)](https://pypi.org/project/topasmoo/)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+
@@ -0,0 +1,106 @@
1
+ # TopasMOO
2
+
3
+ [![CI](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml/badge.svg)](https://github.com/mindyharkness/TopasMOO/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/topasmoo.svg)](https://pypi.org/project/topasmoo/)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/topasmoo.svg)](https://pypi.org/project/topasmoo/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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