aiaccel 2025.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiaccel/__init__.py +3 -0
- aiaccel/config/__init__.py +19 -0
- aiaccel/config/config.py +181 -0
- aiaccel/config/git.py +155 -0
- aiaccel/hpo/__init__.py +0 -0
- aiaccel/hpo/algorithms/__init__.py +7 -0
- aiaccel/hpo/algorithms/nelder_mead_algorithm.py +282 -0
- aiaccel/hpo/apps/__init__.py +1 -0
- aiaccel/hpo/apps/config/__init__.py +0 -0
- aiaccel/hpo/apps/config/default.yaml +14 -0
- aiaccel/hpo/apps/config/resumable.yaml +8 -0
- aiaccel/hpo/apps/optimize.py +182 -0
- aiaccel/hpo/optuna/__init__.py +0 -0
- aiaccel/hpo/optuna/samplers/__init__.py +3 -0
- aiaccel/hpo/optuna/samplers/nelder_mead_sampler.py +303 -0
- aiaccel/hpo/optuna/suggest_wrapper.py +88 -0
- aiaccel/torch/__init__.py +0 -0
- aiaccel/torch/apps/__init__.py +0 -0
- aiaccel/torch/apps/config/train_base.yaml +9 -0
- aiaccel/torch/apps/config/train_ddp.yaml +9 -0
- aiaccel/torch/apps/train.py +68 -0
- aiaccel/torch/datasets/__init__.py +12 -0
- aiaccel/torch/datasets/cached_dataset.py +113 -0
- aiaccel/torch/datasets/file_cached_dataset.py +55 -0
- aiaccel/torch/datasets/hdf5_dataset.py +94 -0
- aiaccel/torch/datasets/scatter_dataset.py +46 -0
- aiaccel/torch/h5py/__init__.py +5 -0
- aiaccel/torch/h5py/hdf5_writer.py +181 -0
- aiaccel/torch/lightning/__init__.py +4 -0
- aiaccel/torch/lightning/abci_environment.py +67 -0
- aiaccel/torch/lightning/datamodules/__init__.py +3 -0
- aiaccel/torch/lightning/datamodules/single_datamodule.py +95 -0
- aiaccel/torch/lightning/opt_lightning_module.py +66 -0
- aiaccel-2025.4.dist-info/METADATA +80 -0
- aiaccel-2025.4.dist-info/RECORD +38 -0
- aiaccel-2025.4.dist-info/WHEEL +4 -0
- aiaccel-2025.4.dist-info/entry_points.txt +4 -0
- aiaccel-2025.4.dist-info/licenses/LICENSE +21 -0
aiaccel/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from aiaccel.config.config import (
|
|
2
|
+
load_config,
|
|
3
|
+
overwrite_omegaconf_dumper,
|
|
4
|
+
pathlib2str_config,
|
|
5
|
+
print_config,
|
|
6
|
+
resolve_inherit,
|
|
7
|
+
)
|
|
8
|
+
from aiaccel.config.git import PackageGitStatus, collect_git_status_from_config, print_git_status
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"load_config",
|
|
12
|
+
"overwrite_omegaconf_dumper",
|
|
13
|
+
"pathlib2str_config",
|
|
14
|
+
"print_config",
|
|
15
|
+
"resolve_inherit",
|
|
16
|
+
"PackageGitStatus",
|
|
17
|
+
"collect_git_status_from_config",
|
|
18
|
+
"print_git_status",
|
|
19
|
+
]
|
aiaccel/config/config.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from colorama import Fore
|
|
9
|
+
from omegaconf import DictConfig, ListConfig
|
|
10
|
+
from omegaconf import OmegaConf as oc # noqa:N813
|
|
11
|
+
from omegaconf._utils import OmegaConfDumper
|
|
12
|
+
|
|
13
|
+
from yaml import Node
|
|
14
|
+
from yaml.resolver import BaseResolver
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def overwrite_omegaconf_dumper(mode: str = "|") -> None:
|
|
18
|
+
"""
|
|
19
|
+
Overwrites the default string representation in OmegaConf's YAML dumper.
|
|
20
|
+
|
|
21
|
+
This function modifies the `OmegaConfDumper` to represent multi-line strings
|
|
22
|
+
using the specified style (`mode`). By default, it uses the `|` block style
|
|
23
|
+
for multi-line strings. Single-line strings remain unchanged.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
mode (str, optional): The YAML style character for multi-line strings.
|
|
27
|
+
Defaults to "|".
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def str_representer(dumper: OmegaConfDumper, data: str) -> Node:
|
|
31
|
+
return dumper.represent_scalar(
|
|
32
|
+
BaseResolver.DEFAULT_SCALAR_TAG, data, style=mode if len(data.splitlines()) > 1 else None
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
OmegaConfDumper.add_representer(str, str_representer)
|
|
36
|
+
OmegaConfDumper.str_representer_added = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_inherit(config: DictConfig | ListConfig) -> DictConfig | ListConfig:
|
|
40
|
+
"""Resolve _inherit_ in config
|
|
41
|
+
|
|
42
|
+
Merge the dict in ``_inherit_`` into a dict of the same hierarchy.
|
|
43
|
+
``_inherit_`` is specified by omegaconf interpolation
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config (DictConfig | ListConfig): The configuration loaded by load_config
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
DictConfig | ListConfig: The configuration without ``_inherit_``
|
|
50
|
+
"""
|
|
51
|
+
while isinstance(config, DictConfig) and "_inherit_" in config:
|
|
52
|
+
# resolve _inherit_
|
|
53
|
+
inherit_configs = config["_inherit_"]
|
|
54
|
+
if not isinstance(inherit_configs, ListConfig):
|
|
55
|
+
inherit_configs = [inherit_configs]
|
|
56
|
+
|
|
57
|
+
config.pop("_inherit_")
|
|
58
|
+
|
|
59
|
+
for inherit_config in inherit_configs:
|
|
60
|
+
if isinstance(inherit_config, DictConfig):
|
|
61
|
+
config = oc.merge(inherit_config, config)
|
|
62
|
+
|
|
63
|
+
# load child DictConfig to resolve child _inherit_
|
|
64
|
+
if isinstance(config, DictConfig):
|
|
65
|
+
dst_config_dict = copy.deepcopy(config)
|
|
66
|
+
for key in config:
|
|
67
|
+
dst_config_dict[key] = resolve_inherit(config[key])
|
|
68
|
+
config = dst_config_dict
|
|
69
|
+
elif isinstance(config, ListConfig):
|
|
70
|
+
dst_config_list = copy.deepcopy(config)
|
|
71
|
+
for key in range(len(config)):
|
|
72
|
+
dst_config_list[key] = resolve_inherit(config[key])
|
|
73
|
+
config = dst_config_list
|
|
74
|
+
|
|
75
|
+
return config
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_config(
|
|
79
|
+
config_filename: str | Path,
|
|
80
|
+
parent_config: dict[str, Any] | DictConfig | ListConfig | None = None,
|
|
81
|
+
) -> DictConfig | ListConfig:
|
|
82
|
+
"""Load YAML configuration
|
|
83
|
+
|
|
84
|
+
When the user specifies ``_base_``, the specified YAML file is loaded as the base,
|
|
85
|
+
and the original configuration is merged with the base config.
|
|
86
|
+
If the configuration specified in ``_base_`` also contains ``_base_``, the process is handled recursively.
|
|
87
|
+
|
|
88
|
+
Additionally, if `bootstrap_config` is provided, it is merged with the final
|
|
89
|
+
configuration to ensure any default values or overrides are applied.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
config (Path): Path to the configuration
|
|
93
|
+
parent_config (dict[str, Any] | DictConfig | ListConfig | None):
|
|
94
|
+
A configuration that is merged to the loaded configuration.
|
|
95
|
+
This is intended to define default config paths (e.g., working_directory) dynamically.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
merge_user_config (DictConfig): The merged configuration of the base config and the original config
|
|
99
|
+
user_config(DictConfig | ListConfig) : The configuration without ``_base_``
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
if not isinstance(config_filename, Path):
|
|
104
|
+
config_filename = Path(config_filename)
|
|
105
|
+
|
|
106
|
+
if not config_filename.is_absolute():
|
|
107
|
+
config_filename = Path.cwd() / config_filename
|
|
108
|
+
|
|
109
|
+
if parent_config is None:
|
|
110
|
+
parent_config = {}
|
|
111
|
+
|
|
112
|
+
config = oc.merge(oc.load(config_filename), parent_config)
|
|
113
|
+
|
|
114
|
+
if isinstance(config, DictConfig) and "_base_" in config:
|
|
115
|
+
# process _base_
|
|
116
|
+
base_paths = config["_base_"]
|
|
117
|
+
if not isinstance(base_paths, ListConfig):
|
|
118
|
+
base_paths = [base_paths]
|
|
119
|
+
|
|
120
|
+
config.pop("_base_")
|
|
121
|
+
for base_path in map(Path, base_paths):
|
|
122
|
+
if not base_path.is_absolute():
|
|
123
|
+
base_path = config_filename.parent / base_path
|
|
124
|
+
|
|
125
|
+
config = load_config(base_path, config)
|
|
126
|
+
|
|
127
|
+
return config
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def print_config(config: ListConfig | DictConfig, line_length: int = 80) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Print the given configuration with syntax highlighting.
|
|
133
|
+
|
|
134
|
+
This function converts `pathlib.Path` objects to strings before printing,
|
|
135
|
+
ensuring that the output YAML format remains valid. It also highlights
|
|
136
|
+
configuration keys in yellow for better readability.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config (ListConfig | DictConfig): The configuration to print.
|
|
140
|
+
line_length (int, optional): The width of the separator line (default: 80).
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
config = pathlib2str_config(config) # https://github.com/omry/omegaconf/issues/82
|
|
145
|
+
|
|
146
|
+
print("=" * line_length)
|
|
147
|
+
for line in oc.to_yaml(config).splitlines():
|
|
148
|
+
print(re.sub(r"(\s*)(\w+):", rf"\1{Fore.YELLOW}\2{Fore.RESET}:", line, count=1))
|
|
149
|
+
print("=" * line_length)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def pathlib2str_config(config: ListConfig | DictConfig) -> ListConfig | DictConfig:
|
|
153
|
+
"""
|
|
154
|
+
Convert `pathlib.Path` objects in the configuration to strings.
|
|
155
|
+
|
|
156
|
+
This function recursively traverses the configuration and replaces all `pathlib.Path`
|
|
157
|
+
objects with their string representations. This is useful for saving the configuration
|
|
158
|
+
in a YAML file, as YAML does not support `Path` objects.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
config (ListConfig | DictConfig): The configuration to convert.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
ListConfig | DictConfig: The modified configuration with `Path` objects replaced by strings.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def _inner_fn(config: ListConfig | DictConfig) -> ListConfig | DictConfig:
|
|
169
|
+
if isinstance(config, ListConfig):
|
|
170
|
+
for ii in range(len(config)):
|
|
171
|
+
config[ii] = _inner_fn(config[ii])
|
|
172
|
+
elif isinstance(config, DictConfig):
|
|
173
|
+
for k, v in config.items():
|
|
174
|
+
if isinstance(v, ListConfig | DictConfig):
|
|
175
|
+
config[k] = _inner_fn(v)
|
|
176
|
+
elif isinstance(v, Path):
|
|
177
|
+
config[k] = str(v)
|
|
178
|
+
|
|
179
|
+
return config
|
|
180
|
+
|
|
181
|
+
return _inner_fn(deepcopy(config))
|
aiaccel/config/git.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from omegaconf import DictConfig, ListConfig
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"PackageGitStatus",
|
|
11
|
+
"collect_git_status_from_config",
|
|
12
|
+
"print_git_status",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PackageGitStatus:
|
|
18
|
+
"""
|
|
19
|
+
Represents the Git status of a package.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
package_name (str): The name of the package.
|
|
23
|
+
commit_id (str): The current Git commit ID of the repository.
|
|
24
|
+
status (list[str]): A list of uncommitted files in the repository.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
package_name: str
|
|
28
|
+
commit_id: str
|
|
29
|
+
status: list[str]
|
|
30
|
+
|
|
31
|
+
def ready(self) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Determines if there are no uncommitted changes.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
bool: True if there are no uncommitted files, otherwise False.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
return len(self.status) == 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def collect_git_status_from_config(config: DictConfig | ListConfig) -> list[PackageGitStatus]:
|
|
43
|
+
"""
|
|
44
|
+
Collects the Git status of packages specified in the given configuration.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config (DictConfig | ListConfig): The configuration containing package references.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
list[PackageGitStatus]: A list of `PackageGitStatus` objects representing
|
|
51
|
+
the Git status of the detected packages.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
status_list = []
|
|
55
|
+
|
|
56
|
+
package_names = collect_target_packages(config)
|
|
57
|
+
package_names.sort()
|
|
58
|
+
|
|
59
|
+
for package_name in package_names:
|
|
60
|
+
status = get_git_status(package_name)
|
|
61
|
+
|
|
62
|
+
if status is not None:
|
|
63
|
+
status_list.append(status)
|
|
64
|
+
|
|
65
|
+
return status_list
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def print_git_status(status: PackageGitStatus | list[PackageGitStatus]) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Prints the Git status of a package or a list of packages.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
status (PackageGitStatus | list[PackageGitStatus]): The Git status to print.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
status_list = status if isinstance(status, list) else [status]
|
|
77
|
+
|
|
78
|
+
for status in status_list:
|
|
79
|
+
print(f"{status.package_name} @ {status.commit_id}")
|
|
80
|
+
for st in status.status:
|
|
81
|
+
print(f" {st}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_git_status(package_name: str) -> PackageGitStatus | None:
|
|
85
|
+
"""
|
|
86
|
+
Retrieves the Git status of a given package.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
package_name (str): The name of the package to check.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
PackageGitStatus | None: A `PackageGitStatus` object if the package is found
|
|
93
|
+
and under Git control, otherwise None.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
# get package location
|
|
97
|
+
spec = importlib.util.find_spec(package_name)
|
|
98
|
+
|
|
99
|
+
if spec is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
if spec.origin is not None:
|
|
103
|
+
module_path = Path(spec.origin).parent
|
|
104
|
+
elif spec.submodule_search_locations is not None:
|
|
105
|
+
module_path = Path(os.path.abspath(spec.submodule_search_locations[0]))
|
|
106
|
+
else:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
# get repository path
|
|
110
|
+
result = subprocess.run(["git", "rev-parse", "--show-toplevel"], cwd=module_path, capture_output=True, text=True)
|
|
111
|
+
if result.returncode != 0:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
repository_path = result.stdout.splitlines()[0]
|
|
115
|
+
|
|
116
|
+
# get commit id
|
|
117
|
+
result = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repository_path, capture_output=True, text=True)
|
|
118
|
+
commit_id = result.stdout.splitlines()[0]
|
|
119
|
+
|
|
120
|
+
# check git status
|
|
121
|
+
result = subprocess.run(["git", "status", "-s"], cwd=repository_path, capture_output=True, text=True)
|
|
122
|
+
status = result.stdout.splitlines()
|
|
123
|
+
|
|
124
|
+
return PackageGitStatus(package_name, commit_id, status)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def collect_target_packages(config: ListConfig | DictConfig) -> list[str]:
|
|
128
|
+
"""
|
|
129
|
+
Extracts the names of target packages from the given configuration.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
config (ListConfig | DictConfig): The configuration to process.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
list[str]: A list of package names extracted from the configuration.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
target_packages = set()
|
|
139
|
+
|
|
140
|
+
def inner_func(_config: ListConfig | DictConfig) -> None:
|
|
141
|
+
if isinstance(_config, DictConfig):
|
|
142
|
+
for key, value in _config.items():
|
|
143
|
+
if key == "_target_":
|
|
144
|
+
package_name, *_ = value.split(".")
|
|
145
|
+
target_packages.add(package_name)
|
|
146
|
+
|
|
147
|
+
inner_func(value)
|
|
148
|
+
|
|
149
|
+
elif isinstance(_config, ListConfig):
|
|
150
|
+
for item in _config:
|
|
151
|
+
inner_func(item)
|
|
152
|
+
|
|
153
|
+
inner_func(config)
|
|
154
|
+
|
|
155
|
+
return list(target_packages)
|
aiaccel/hpo/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import numpy.typing as npt
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import queue
|
|
6
|
+
import threading
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class NelderMeadCoefficient:
|
|
13
|
+
r: float = 1.0
|
|
14
|
+
ic: float = -0.5
|
|
15
|
+
oc: float = 0.5
|
|
16
|
+
e: float = 2.0
|
|
17
|
+
s: float = 0.5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NelderMeadEmptyError(Exception):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class UnexpectedVerticesUpdateError(Exception):
|
|
26
|
+
updated_vertices: list[npt.NDArray[np.float64]]
|
|
27
|
+
updated_values: list[float]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NelderMeadAlgorism:
|
|
31
|
+
"""Class to manage the NelderMead algorithm
|
|
32
|
+
|
|
33
|
+
Uses a queue to receive results and advance the NelderMead algorithm.
|
|
34
|
+
Return parameters within the normalization range by referring only to the number of dimensions.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
dimensions: int | None = None
|
|
38
|
+
The number of dimensions in the search space.
|
|
39
|
+
coeff: NelderMeadCoefficient | None = None
|
|
40
|
+
Parameters used in NelderMead.
|
|
41
|
+
rng: np.random.RandomState | None = None
|
|
42
|
+
RandomState used for calculating initial points.
|
|
43
|
+
block: bool = False
|
|
44
|
+
Sets whether to block the queue used internally.
|
|
45
|
+
timeout: int | None = None
|
|
46
|
+
Time to block the queue.
|
|
47
|
+
Attributes:
|
|
48
|
+
vertices: list[npt.NDArray[np.float64]]
|
|
49
|
+
List of simplex parameters.
|
|
50
|
+
values: list[float]
|
|
51
|
+
List of simplex calculation results.
|
|
52
|
+
generator: iterator
|
|
53
|
+
Generator for NelderMead parameters.
|
|
54
|
+
lock: threading.Lock
|
|
55
|
+
threading.Lock variable used for thread-safe processing.
|
|
56
|
+
results: queue.Queue[tuple[npt.NDArray[np.float64], float, bool]]
|
|
57
|
+
Queue to receive tuples of parameters, calculation results,
|
|
58
|
+
and a boolean indicating whether the parameters were output by NelderMead.
|
|
59
|
+
simplex_size: int
|
|
60
|
+
Number of vertices in the simplex.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
vertices: list[npt.NDArray[np.float64]]
|
|
64
|
+
values: list[float]
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
dimensions: int | None = None,
|
|
69
|
+
coeff: NelderMeadCoefficient | None = None,
|
|
70
|
+
rng: np.random.RandomState | None = None,
|
|
71
|
+
block: bool = False,
|
|
72
|
+
timeout: int | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.coeff = coeff if coeff is not None else NelderMeadCoefficient()
|
|
75
|
+
|
|
76
|
+
self._rng = rng if rng is not None else np.random.RandomState()
|
|
77
|
+
|
|
78
|
+
self.generator = iter(self._generator())
|
|
79
|
+
self.lock = threading.Lock()
|
|
80
|
+
|
|
81
|
+
self.results: queue.Queue[tuple[npt.NDArray[np.float64], float, bool]] = queue.Queue()
|
|
82
|
+
|
|
83
|
+
self.block = block
|
|
84
|
+
self.timeout = timeout
|
|
85
|
+
|
|
86
|
+
self.dimensions = dimensions
|
|
87
|
+
|
|
88
|
+
def get_vertex(self, dimensions: int | None = None) -> npt.NDArray[np.float64]:
|
|
89
|
+
"""Method to return the next parameters for NelderMead
|
|
90
|
+
|
|
91
|
+
Thread-safe due to parallel processing requirements.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
npt.NDArray[np.float64]:
|
|
95
|
+
The next parameters for NelderMead.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if dimensions is not None:
|
|
99
|
+
if self.dimensions is None:
|
|
100
|
+
self.dimensions = dimensions
|
|
101
|
+
else:
|
|
102
|
+
assert self.dimensions == dimensions
|
|
103
|
+
elif dimensions is None and self.dimensions is None:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"dimensions is not set yet. Please provide it on __init__ or get_vertex or call put_vertex in advance."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
with self.lock:
|
|
109
|
+
for vertex in self.generator:
|
|
110
|
+
if vertex is None:
|
|
111
|
+
raise NelderMeadEmptyError(
|
|
112
|
+
"Cannot generate new vertex now. Maybe get_vertex is called in parallel."
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if all(0 < x < 1 for x in vertex):
|
|
116
|
+
break
|
|
117
|
+
self.put_value(vertex, np.inf)
|
|
118
|
+
|
|
119
|
+
assert vertex is not None
|
|
120
|
+
|
|
121
|
+
return vertex
|
|
122
|
+
|
|
123
|
+
def put_value(
|
|
124
|
+
self,
|
|
125
|
+
vertex: npt.NDArray[np.float64],
|
|
126
|
+
value: float,
|
|
127
|
+
enqueue: bool = False,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Method to pass a pair of parameters and results to NelderMead
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
vertex: npt.NDArray[np.float64]: Parameters
|
|
133
|
+
value: float: Calculation result
|
|
134
|
+
enqueue: bool = False:
|
|
135
|
+
Boolean indicating whether the parameters were output by NelderMead.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
if self.dimensions is None:
|
|
139
|
+
self.dimensions = len(vertex)
|
|
140
|
+
else:
|
|
141
|
+
assert self.dimensions == len(vertex)
|
|
142
|
+
|
|
143
|
+
self.results.put((vertex, value, enqueue))
|
|
144
|
+
|
|
145
|
+
def _collect_enqueued_results(
|
|
146
|
+
self,
|
|
147
|
+
vertices: list[npt.NDArray[np.float64]] | None = None,
|
|
148
|
+
values: list[float] | None = None,
|
|
149
|
+
) -> tuple[list[npt.NDArray[np.float64]], list[float]]:
|
|
150
|
+
vertices = [] if vertices is None else vertices
|
|
151
|
+
values = [] if values is None else values
|
|
152
|
+
|
|
153
|
+
while True:
|
|
154
|
+
try:
|
|
155
|
+
vertex, value, enqueue = self.results.get(block=False)
|
|
156
|
+
assert enqueue
|
|
157
|
+
|
|
158
|
+
vertices.append(vertex)
|
|
159
|
+
values.append(value)
|
|
160
|
+
except queue.Empty:
|
|
161
|
+
break
|
|
162
|
+
|
|
163
|
+
return vertices, values
|
|
164
|
+
|
|
165
|
+
def _wait_for_results(
|
|
166
|
+
self,
|
|
167
|
+
num_waiting: int,
|
|
168
|
+
) -> Generator[None, None, tuple[list[npt.NDArray[np.float64]], list[float]]]:
|
|
169
|
+
# collect results
|
|
170
|
+
vertices, values = list[npt.NDArray[np.float64]](), list[float]()
|
|
171
|
+
enqueued_vertices, enqueued_values = list[npt.NDArray[np.float64]](), list[float]()
|
|
172
|
+
while len(values) < num_waiting:
|
|
173
|
+
try:
|
|
174
|
+
vertex, value, enqueue = self.results.get(block=self.block, timeout=self.timeout)
|
|
175
|
+
if enqueue:
|
|
176
|
+
enqueued_vertices.append(vertex)
|
|
177
|
+
enqueued_values.append(value)
|
|
178
|
+
else:
|
|
179
|
+
vertices.append(vertex)
|
|
180
|
+
values.append(value)
|
|
181
|
+
except queue.Empty:
|
|
182
|
+
yield None
|
|
183
|
+
|
|
184
|
+
enqueued_vertices, enqueued_values = self._collect_enqueued_results(enqueued_vertices, enqueued_values)
|
|
185
|
+
|
|
186
|
+
# check if enqueued vertices change ordering
|
|
187
|
+
if (len(self.values) == 0 and len(enqueued_values) > 0) or (
|
|
188
|
+
len(self.values) > 0 and len(enqueued_values) > 0 and min(enqueued_values) < max(self.values)
|
|
189
|
+
):
|
|
190
|
+
new_vertices = self.vertices + vertices + enqueued_vertices
|
|
191
|
+
new_values = self.values + values + enqueued_values
|
|
192
|
+
|
|
193
|
+
raise UnexpectedVerticesUpdateError(new_vertices, new_values)
|
|
194
|
+
|
|
195
|
+
return vertices, values
|
|
196
|
+
|
|
197
|
+
def _wait_for_result(
|
|
198
|
+
self,
|
|
199
|
+
) -> Generator[None, None, float]:
|
|
200
|
+
_, values = yield from self._wait_for_results(1)
|
|
201
|
+
return values[0]
|
|
202
|
+
|
|
203
|
+
def _generator(self) -> Generator[npt.NDArray[np.float64] | None, None, None]: # noqa: C901
|
|
204
|
+
# initialization
|
|
205
|
+
|
|
206
|
+
self.vertices, self.values = self._collect_enqueued_results()
|
|
207
|
+
|
|
208
|
+
if self.dimensions is None:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
"dimensions is not set yet. Please provide it on __init__ or get_vertex or call put_vertex in advance."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if self.dimensions + 1 > len(self.vertices):
|
|
214
|
+
try:
|
|
215
|
+
num_random_points = self.dimensions + 1 - len(self.vertices)
|
|
216
|
+
|
|
217
|
+
random_vertices_shape = (num_random_points, self.dimensions)
|
|
218
|
+
random_vertices: list[npt.NDArray[np.float64]] = list(self._rng.uniform(0, 1, random_vertices_shape))
|
|
219
|
+
yield from random_vertices
|
|
220
|
+
|
|
221
|
+
random_vertices, random_values = yield from self._wait_for_results(num_random_points)
|
|
222
|
+
|
|
223
|
+
self.vertices = self.vertices + random_vertices
|
|
224
|
+
self.values = self.values + random_values
|
|
225
|
+
except UnexpectedVerticesUpdateError as e:
|
|
226
|
+
self.vertices, self.values = e.updated_vertices, e.updated_values
|
|
227
|
+
|
|
228
|
+
# main loop
|
|
229
|
+
shrink_requied = False
|
|
230
|
+
while True:
|
|
231
|
+
try:
|
|
232
|
+
# sort self.vertices by their self.values
|
|
233
|
+
order = np.argsort(self.values)[: self.dimensions + 1]
|
|
234
|
+
self.vertices = [self.vertices[idx] for idx in order]
|
|
235
|
+
self.values = [self.values[idx] for idx in order]
|
|
236
|
+
|
|
237
|
+
# reflect
|
|
238
|
+
yc = np.mean(self.vertices[:-1], axis=0)
|
|
239
|
+
yield (yr := yc + self.coeff.r * (yc - self.vertices[-1]))
|
|
240
|
+
|
|
241
|
+
fr = yield from self._wait_for_result()
|
|
242
|
+
|
|
243
|
+
if self.values[0] <= fr < self.values[-2]:
|
|
244
|
+
self.vertices[-1], self.values[-1] = yr, fr
|
|
245
|
+
|
|
246
|
+
elif fr < self.values[0]: # expand
|
|
247
|
+
yield (ye := yc + self.coeff.e * (yc - self.vertices[-1]))
|
|
248
|
+
|
|
249
|
+
fe = yield from self._wait_for_result()
|
|
250
|
+
|
|
251
|
+
self.vertices[-1], self.values[-1] = (ye, fe) if fe < fr else (yr, fr)
|
|
252
|
+
|
|
253
|
+
elif self.values[-2] <= fr < self.values[-1]: # outside contract
|
|
254
|
+
yield (yoc := yc + self.coeff.oc * (yc - self.vertices[-1]))
|
|
255
|
+
|
|
256
|
+
foc = yield from self._wait_for_result()
|
|
257
|
+
|
|
258
|
+
if foc <= fr:
|
|
259
|
+
self.vertices[-1], self.values[-1] = yoc, foc
|
|
260
|
+
else:
|
|
261
|
+
shrink_requied = True
|
|
262
|
+
|
|
263
|
+
elif self.values[-1] <= fr: # inside contract
|
|
264
|
+
yield (yic := yc + self.coeff.ic * (yc - self.vertices[-1]))
|
|
265
|
+
|
|
266
|
+
fic = yield from self._wait_for_result()
|
|
267
|
+
|
|
268
|
+
if fic < self.values[-1]:
|
|
269
|
+
self.vertices[-1], self.values[-1] = yic, fic
|
|
270
|
+
else:
|
|
271
|
+
shrink_requied = True
|
|
272
|
+
|
|
273
|
+
# shrink
|
|
274
|
+
if shrink_requied:
|
|
275
|
+
self.vertices = [(v0 := self.vertices[0]) + self.coeff.s * (v - v0) for v in self.vertices]
|
|
276
|
+
yield from self.vertices[1:]
|
|
277
|
+
|
|
278
|
+
self.vertices[1:], self.values[1:] = yield from self._wait_for_results(len(self.vertices[1:]))
|
|
279
|
+
shrink_requied = False
|
|
280
|
+
|
|
281
|
+
except UnexpectedVerticesUpdateError as e:
|
|
282
|
+
self.vertices, self.values = e.updated_vertices, e.updated_values
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
File without changes
|