jcmoptimizer 3.0.9__tar.gz → 3.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/PKG-INFO +1 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/benchmark.py +0 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/client.py +1 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/drivers/__init__.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/drivers/active_learning_drivers.py +47 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/drivers/driver.py +12 -32
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/objects.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/requestor.py +43 -3
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/server.py +39 -38
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/study.py +13 -3
- jcmoptimizer-3.1.0/jcmoptimizer/utils/__init__.py +5 -0
- jcmoptimizer-3.1.0/jcmoptimizer/utils/neural_network.py +142 -0
- jcmoptimizer-3.1.0/jcmoptimizer/version.py +1 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer.egg-info/PKG-INFO +1 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer.egg-info/SOURCES.txt +3 -1
- jcmoptimizer-3.0.9/jcmoptimizer/version.py +0 -1
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/LICENSE +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/README.md +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/__init__.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer/drivers/third_party_drivers.py +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer.egg-info/dependency_links.txt +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer.egg-info/requires.txt +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/jcmoptimizer.egg-info/top_level.txt +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/pyproject.toml +0 -0
- {jcmoptimizer-3.0.9 → jcmoptimizer-3.1.0}/setup.cfg +0 -0
|
@@ -242,7 +242,7 @@ class Client:
|
|
|
242
242
|
|
|
243
243
|
:name: Name of the parameter. E.g. ``'x1'``. The name should contain
|
|
244
244
|
no spaces and must not be equal to function names or mathematical
|
|
245
|
-
constants like ``'sin'``, ``'cos'``, ``'exp'``, ``'
|
|
245
|
+
constants like ``'sin'``, ``'cos'``, ``'exp'``, ``'PI'`` etc.
|
|
246
246
|
:type: Type of the parameter. Either ``'variable'`` or ``'fixed'``.
|
|
247
247
|
Fixed parameters can be used in the constraint functions or other
|
|
248
248
|
expressions.
|
|
File without changes
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
from typing import Any, Optional, Literal
|
|
2
|
-
import
|
|
2
|
+
import base64
|
|
3
3
|
|
|
4
4
|
from .driver import Driver, Minimizer, LeastSquaresDriver
|
|
5
5
|
from ..requestor import OptimizerRequestor
|
|
6
6
|
|
|
7
7
|
class ActiveLearningDriver(Driver):
|
|
8
|
+
def download_neural_network(self, path: str, filename: str) -> None:
|
|
9
|
+
"""Download a trained neural network ensemble artifact.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
path: Dot-separated driver path to the neural network surrogate,
|
|
13
|
+
for example ``"surrogates.0"``.
|
|
14
|
+
filename: Target zip artifact path. The downloaded artifact can be
|
|
15
|
+
loaded with
|
|
16
|
+
:meth:`jcmoptimizer.utils.NeuralNetworkEnsemblePredictor.from_file`.
|
|
17
|
+
"""
|
|
18
|
+
answer = self._post(
|
|
19
|
+
"download neural network",
|
|
20
|
+
"driver",
|
|
21
|
+
"export_neural_network",
|
|
22
|
+
data={"path": path},
|
|
23
|
+
)
|
|
24
|
+
with open(filename, "wb") as f:
|
|
25
|
+
f.write(base64.b64decode(answer["artifact"]))
|
|
26
|
+
|
|
8
27
|
def describe(self) -> dict[str, Any]:
|
|
9
28
|
"""Get description of all modules and their parameters that are used
|
|
10
29
|
by the driver. Example::
|
|
@@ -182,6 +201,11 @@ class ActiveLearningDriver(Driver):
|
|
|
182
201
|
used for training the surrogate are neglected if their distance
|
|
183
202
|
is smaller than min_dist (in terms of the length scales of the
|
|
184
203
|
surrogate).
|
|
204
|
+
|
|
205
|
+
.. note:: This method only optimizes hyperparameters. If a surrogate
|
|
206
|
+
needs to be prepared or a neural network needs to be trained after
|
|
207
|
+
the hyperparameter update, call :func:`train` explicitly. The train
|
|
208
|
+
task saves the processed driver state.
|
|
185
209
|
"""
|
|
186
210
|
self._run_task(
|
|
187
211
|
"optimize hyperparameters",
|
|
@@ -189,6 +213,17 @@ class ActiveLearningDriver(Driver):
|
|
|
189
213
|
data={"path": path, "num_samples": num_samples, "min_dist": min_dist},
|
|
190
214
|
)
|
|
191
215
|
|
|
216
|
+
def train(self, path: Optional[str] = None) -> None:
|
|
217
|
+
"""Prepare/train surrogate models explicitly. Example::
|
|
218
|
+
|
|
219
|
+
driver.train("driver.surrogates.0")
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
path: A dot-separated path to a surrogate. If no path is specified,
|
|
223
|
+
all surrogates are trained/prepared.
|
|
224
|
+
"""
|
|
225
|
+
self._run_task("train", "train_surrogates", data={"path": path})
|
|
226
|
+
|
|
192
227
|
def get_minima(
|
|
193
228
|
self,
|
|
194
229
|
object_type: Literal["surrogate", "variable", "objective"] = "objective",
|
|
@@ -822,6 +857,17 @@ class BayesianLeastSquares(SingleGPSingleVariable, LeastSquaresDriver):
|
|
|
822
857
|
)
|
|
823
858
|
return answer["state"]
|
|
824
859
|
|
|
860
|
+
@property
|
|
861
|
+
def covariance(self) -> list[list[float]]:
|
|
862
|
+
"""Covariance matrix of the reconstructed continuous parameters."""
|
|
863
|
+
answer = self._post(
|
|
864
|
+
"get parameter covariance",
|
|
865
|
+
"driver",
|
|
866
|
+
"get_state",
|
|
867
|
+
data={"path": "variables.0.parameter_covariance"},
|
|
868
|
+
)
|
|
869
|
+
return answer["state"]
|
|
870
|
+
|
|
825
871
|
def predict(
|
|
826
872
|
self,
|
|
827
873
|
points: list[list[float]],
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
from typing import Any, Optional, Union
|
|
2
|
-
import requests
|
|
3
|
-
import time
|
|
4
2
|
|
|
5
3
|
from ..requestor import OptimizerRequestor
|
|
6
4
|
|
|
@@ -45,35 +43,7 @@ class Driver:
|
|
|
45
43
|
return self._requestor.get(purpose, object, type, self.qualifier)
|
|
46
44
|
|
|
47
45
|
def _run_task(self, purpose: str, task: str, data: dict[str, Any]) -> Any:
|
|
48
|
-
|
|
49
|
-
answer = self._post(purpose, "driver", "start_task", data=data)
|
|
50
|
-
|
|
51
|
-
print("\r" + purpose[0].capitalize() + purpose[1:] + ": ", end="")
|
|
52
|
-
task_id = answer["task_id"]
|
|
53
|
-
try:
|
|
54
|
-
while True:
|
|
55
|
-
answer = self._post(
|
|
56
|
-
purpose, "driver", "get_task_status", data={"task_id": task_id}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
progress_msg = answer["progress_msg"]
|
|
60
|
-
status = answer["status"]
|
|
61
|
-
print("\r" + progress_msg + " ", end="")
|
|
62
|
-
|
|
63
|
-
if status in ["stopped", "finished"]:
|
|
64
|
-
print("")
|
|
65
|
-
break
|
|
66
|
-
time.sleep(1)
|
|
67
|
-
|
|
68
|
-
except KeyboardInterrupt:
|
|
69
|
-
answer = self._post(
|
|
70
|
-
purpose, "driver", "stop_task", data={"task_id": task_id}
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
answer = self._post(
|
|
74
|
-
purpose, "driver", "fetch_task_result", data={"task_id": task_id}
|
|
75
|
-
)
|
|
76
|
-
return answer["result"]
|
|
46
|
+
return self._requestor.run_task(purpose, task, self.qualifier, data)
|
|
77
47
|
|
|
78
48
|
def describe(self) -> dict[str, Any]:
|
|
79
49
|
"""Get description of all modules and their parameters that are used
|
|
@@ -129,7 +99,6 @@ class Driver:
|
|
|
129
99
|
)
|
|
130
100
|
return answer["values"]
|
|
131
101
|
|
|
132
|
-
|
|
133
102
|
class Minimizer(Driver):
|
|
134
103
|
"""
|
|
135
104
|
This class provides methods for retrieving and setting information of the
|
|
@@ -210,3 +179,14 @@ class LeastSquaresDriver(Minimizer):
|
|
|
210
179
|
data={"path": "parameter_uncertainties"},
|
|
211
180
|
)
|
|
212
181
|
return answer["state"]
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def covariance(self) -> list[list[float]]:
|
|
185
|
+
"""Covariance matrix of the reconstructed continuous parameters."""
|
|
186
|
+
answer = self._post(
|
|
187
|
+
"get parameter covariance",
|
|
188
|
+
"driver",
|
|
189
|
+
"get_state",
|
|
190
|
+
data={"path": "parameter_covariance"},
|
|
191
|
+
)
|
|
192
|
+
return answer["state"]
|
|
File without changes
|
|
@@ -6,7 +6,6 @@ from datetime import datetime as dt
|
|
|
6
6
|
from datetime import timedelta
|
|
7
7
|
import json
|
|
8
8
|
import itertools
|
|
9
|
-
import threading
|
|
10
9
|
import time
|
|
11
10
|
|
|
12
11
|
template = 'Please install the package {p} (e.g. run "pip install {p}") .'
|
|
@@ -141,7 +140,7 @@ def parse_yaml(f: TextIO) -> dict[str, Any]:
|
|
|
141
140
|
"Expected format: key:value\n"
|
|
142
141
|
f"Found format: {line}"
|
|
143
142
|
)
|
|
144
|
-
except SyntaxError
|
|
143
|
+
except SyntaxError:
|
|
145
144
|
raise SyntaxError(
|
|
146
145
|
f"Invalid format of config file "
|
|
147
146
|
f"at {f.name}:{line_num + 1}.\n"
|
|
@@ -206,7 +205,7 @@ class Requestor:
|
|
|
206
205
|
else:
|
|
207
206
|
raise NotImplementedError
|
|
208
207
|
|
|
209
|
-
except requests.exceptions.ConnectionError
|
|
208
|
+
except requests.exceptions.ConnectionError:
|
|
210
209
|
raise ConnectionError(
|
|
211
210
|
f"Could not connect to server at URL {self.endpoint}. "
|
|
212
211
|
"Please, check your internet connection and that the server is running."
|
|
@@ -366,3 +365,44 @@ class OptimizerRequestor(Requestor):
|
|
|
366
365
|
r = self.request("POST", purpose, url, data=data)
|
|
367
366
|
self._print_messages(r)
|
|
368
367
|
return r.json()
|
|
368
|
+
|
|
369
|
+
def run_task(
|
|
370
|
+
self,
|
|
371
|
+
purpose: str,
|
|
372
|
+
task: str,
|
|
373
|
+
id: str,
|
|
374
|
+
data: dict[str, Any],
|
|
375
|
+
) -> Any:
|
|
376
|
+
data["task"] = task
|
|
377
|
+
answer = self.post(purpose, "driver", "start_task", id=id, data=data)
|
|
378
|
+
|
|
379
|
+
print("\r" + purpose[0].capitalize() + purpose[1:] + ": ", end="")
|
|
380
|
+
task_id = answer["task_id"]
|
|
381
|
+
try:
|
|
382
|
+
while True:
|
|
383
|
+
answer = self.post(
|
|
384
|
+
purpose,
|
|
385
|
+
"driver",
|
|
386
|
+
"get_task_status",
|
|
387
|
+
id=id,
|
|
388
|
+
data={"task_id": task_id},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
progress_msg = answer["progress_msg"]
|
|
392
|
+
status = answer["status"]
|
|
393
|
+
print("\r" + progress_msg + " ", end="")
|
|
394
|
+
|
|
395
|
+
if status in ["stopped", "finished"]:
|
|
396
|
+
print("")
|
|
397
|
+
break
|
|
398
|
+
time.sleep(1)
|
|
399
|
+
|
|
400
|
+
except KeyboardInterrupt:
|
|
401
|
+
self.post(
|
|
402
|
+
purpose, "driver", "stop_task", id=id, data={"task_id": task_id}
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
answer = self.post(
|
|
406
|
+
purpose, "driver", "fetch_task_result", id=id, data={"task_id": task_id}
|
|
407
|
+
)
|
|
408
|
+
return answer["result"]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
from typing import Any, Optional,
|
|
2
|
-
from io import BufferedRandom
|
|
1
|
+
from typing import Any, Optional, Literal
|
|
3
2
|
import os
|
|
4
3
|
import sys
|
|
5
4
|
import json
|
|
@@ -8,11 +7,9 @@ import time
|
|
|
8
7
|
import subprocess
|
|
9
8
|
from datetime import datetime, timedelta
|
|
10
9
|
import atexit
|
|
11
|
-
import requests
|
|
12
10
|
|
|
13
11
|
from .requestor import (
|
|
14
12
|
read_yml_config_files,
|
|
15
|
-
warn,
|
|
16
13
|
inform,
|
|
17
14
|
CloudRequestor,
|
|
18
15
|
show_working,
|
|
@@ -65,7 +62,7 @@ def _wait_for_server_startup(
|
|
|
65
62
|
t0 = time.time()
|
|
66
63
|
while time.time() - t0 < STARTUP_TIMEOUT:
|
|
67
64
|
try:
|
|
68
|
-
|
|
65
|
+
Client(
|
|
69
66
|
host=host,
|
|
70
67
|
verbose=False,
|
|
71
68
|
cloud_endpoint=cloud_endpoint,
|
|
@@ -343,7 +340,7 @@ class Server:
|
|
|
343
340
|
def _shutdown_at_exit(self) -> None:
|
|
344
341
|
try:
|
|
345
342
|
self.shutdown(force=True)
|
|
346
|
-
except ConnectionError
|
|
343
|
+
except ConnectionError:
|
|
347
344
|
pass
|
|
348
345
|
|
|
349
346
|
def _start_local_server(
|
|
@@ -389,12 +386,12 @@ class Server:
|
|
|
389
386
|
env["JCM_LICENSE_DIR"] = license_dir
|
|
390
387
|
|
|
391
388
|
# Start JCMoptimizer
|
|
392
|
-
cmd
|
|
389
|
+
cmd = [jcmoptimizer_exe]
|
|
393
390
|
if port is not None:
|
|
394
|
-
cmd.
|
|
391
|
+
cmd.extend(["--port", str(port)])
|
|
395
392
|
cmd.append("--print_json")
|
|
396
393
|
if not persistent:
|
|
397
|
-
cmd.
|
|
394
|
+
cmd.extend(["--calling_pid", str(os.getpid())])
|
|
398
395
|
close_fds = os.name != "nt"
|
|
399
396
|
|
|
400
397
|
# Generate temporary files for errors and log
|
|
@@ -402,9 +399,8 @@ class Server:
|
|
|
402
399
|
tempfile.TemporaryFile() as error_file,
|
|
403
400
|
tempfile.TemporaryFile() as log_file,
|
|
404
401
|
):
|
|
405
|
-
Popen(
|
|
406
|
-
|
|
407
|
-
shell=True,
|
|
402
|
+
server_proc = Popen(
|
|
403
|
+
cmd,
|
|
408
404
|
stdout=log_file,
|
|
409
405
|
stderr=error_file,
|
|
410
406
|
close_fds=close_fds,
|
|
@@ -414,36 +410,41 @@ class Server:
|
|
|
414
410
|
env=env,
|
|
415
411
|
)
|
|
416
412
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
try:
|
|
414
|
+
# Poll process for new output until first line with port information
|
|
415
|
+
line = b""
|
|
416
|
+
for _ in range(round(10 * timeout)):
|
|
417
|
+
error_file.seek(0)
|
|
418
|
+
if len(error_file.readlines()):
|
|
419
|
+
response = _server_response(log_file, error_file)
|
|
420
|
+
raise EnvironmentError(
|
|
421
|
+
"Could not start optimization server. "
|
|
422
|
+
f"Server response: \n{response}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
log_file.seek(0)
|
|
426
|
+
for line in iter(log_file.readline, b""):
|
|
427
|
+
if line[:17] == b'{"optimizer_port"':
|
|
428
|
+
break
|
|
429
|
+
else:
|
|
430
|
+
time.sleep(0.1)
|
|
431
|
+
continue
|
|
432
|
+
break
|
|
433
|
+
else:
|
|
422
434
|
response = _server_response(log_file, error_file)
|
|
435
|
+
raise TimeoutError(response)
|
|
436
|
+
response = _server_response(log_file, error_file)
|
|
437
|
+
try:
|
|
438
|
+
info = json.loads(line)
|
|
439
|
+
except Exception as err:
|
|
423
440
|
raise EnvironmentError(
|
|
424
441
|
"Could not start optimization server. "
|
|
425
442
|
f"Server response: \n{response}"
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
break
|
|
432
|
-
else:
|
|
433
|
-
time.sleep(0.1)
|
|
434
|
-
continue
|
|
435
|
-
break
|
|
436
|
-
else:
|
|
437
|
-
response = _server_response(log_file, error_file)
|
|
438
|
-
raise TimeoutError(response)
|
|
439
|
-
try:
|
|
440
|
-
info = json.loads(line)
|
|
441
|
-
except Exception as err:
|
|
442
|
-
response = _server_response(log_file, error_file)
|
|
443
|
-
raise EnvironmentError(
|
|
444
|
-
"Could not start optimization server. "
|
|
445
|
-
f"Server response: \n{response}"
|
|
446
|
-
) from err
|
|
443
|
+
) from err
|
|
444
|
+
except BaseException:
|
|
445
|
+
server_proc.kill()
|
|
446
|
+
server_proc.wait(timeout=5)
|
|
447
|
+
raise
|
|
447
448
|
|
|
448
449
|
self._port = int(info["optimizer_port"])
|
|
449
450
|
self._pid = int(info["pid"])
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
from typing import Any, Callable, Optional, Union
|
|
2
2
|
import time
|
|
3
3
|
import threading
|
|
4
|
-
import warnings
|
|
5
4
|
import traceback
|
|
6
5
|
import atexit
|
|
7
|
-
import requests
|
|
8
6
|
from datetime import datetime, timedelta
|
|
9
7
|
|
|
10
8
|
from .requestor import OptimizerRequestor, NumParallelError, warn
|
|
@@ -90,6 +88,11 @@ class Study:
|
|
|
90
88
|
self._extend_shutdown_time(self._extend_on_suggestion)
|
|
91
89
|
return result
|
|
92
90
|
|
|
91
|
+
def _run_task(self, purpose: str, task: str, data: dict[str, Any]) -> Any:
|
|
92
|
+
result = self._requestor.run_task(purpose, task, self.qualifier, data)
|
|
93
|
+
self._extend_shutdown_time(self._extend_on_request)
|
|
94
|
+
return result
|
|
95
|
+
|
|
93
96
|
def _get(self, purpose: str, object: str, type: str) -> dict[str, Any]:
|
|
94
97
|
result = self._requestor.get(purpose, object, type, self.qualifier)
|
|
95
98
|
self._extend_shutdown_time(self._extend_on_request)
|
|
@@ -294,6 +297,7 @@ class Study:
|
|
|
294
297
|
"""
|
|
295
298
|
observations: list[Observation] = []
|
|
296
299
|
design_values: list[list[Union[float, str]]] = []
|
|
300
|
+
obs = self.new_observation()
|
|
297
301
|
last_design_value = None
|
|
298
302
|
for idx in range(len(data["value"])):
|
|
299
303
|
design_value = data["design_value"][idx]
|
|
@@ -528,6 +532,13 @@ class Study:
|
|
|
528
532
|
shown on the dashboard. To avoid this, one has to add the observations
|
|
529
533
|
one by one using :func:`add_observation`.
|
|
530
534
|
|
|
535
|
+
.. note:: After adding many observations, we recommend explicitly running
|
|
536
|
+
:meth:`jcmoptimizer.ActiveLearning.optimize_hyperparameters` if desired,
|
|
537
|
+
followed by :meth:`jcmoptimizer.ActiveLearning.train`. This keeps the
|
|
538
|
+
bulk-import driver state clean
|
|
539
|
+
and makes the expensive processing steps and processed-state saving
|
|
540
|
+
explicit.
|
|
541
|
+
|
|
531
542
|
"""
|
|
532
543
|
obs_data = []
|
|
533
544
|
for o in observations:
|
|
@@ -535,7 +546,6 @@ class Study:
|
|
|
535
546
|
raise TypeError("observations -> expected Observation objects.")
|
|
536
547
|
obs_data.append(o.data)
|
|
537
548
|
|
|
538
|
-
# todo: check
|
|
539
549
|
self._post(
|
|
540
550
|
"add observations",
|
|
541
551
|
"observation",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional, Union
|
|
4
|
+
import io
|
|
5
|
+
import json
|
|
6
|
+
import zipfile
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NeuralNetworkEnsemblePredictor:
|
|
13
|
+
"""Client-side predictor for a downloaded neural network ensemble.
|
|
14
|
+
|
|
15
|
+
Use :meth:`from_file` to load an artifact created with
|
|
16
|
+
:meth:`jcmoptimizer.ActiveLearning.download_neural_network`. Predictions are
|
|
17
|
+
evaluated locally with PyTorch and return one prediction per ensemble
|
|
18
|
+
member.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
metadata: dict[str, Any],
|
|
24
|
+
member_model_bytes: bytes,
|
|
25
|
+
member_state_bytes: bytes,
|
|
26
|
+
device: Optional[Union[str, torch.device]] = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Create a predictor from loaded artifact metadata and model bytes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
metadata: Artifact metadata read from ``metadata.json``.
|
|
32
|
+
member_model_bytes: Serialized exported PyTorch member model.
|
|
33
|
+
member_state_bytes: Serialized stacked ensemble member state.
|
|
34
|
+
device: PyTorch device for prediction. If omitted, CUDA is used when
|
|
35
|
+
available and CPU otherwise.
|
|
36
|
+
"""
|
|
37
|
+
if metadata.get("format") != "jcmoptimizer_neural_network_ensemble":
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Unsupported neural network artifact format {metadata.get('format')!r}."
|
|
40
|
+
)
|
|
41
|
+
if metadata.get("artifact_type") != "torch_export_member_vmap":
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"Unsupported neural network artifact type "
|
|
44
|
+
f"{metadata.get('artifact_type')!r}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
import torch
|
|
49
|
+
from torch.func import functional_call, vmap
|
|
50
|
+
except ImportError as error:
|
|
51
|
+
raise ImportError(
|
|
52
|
+
"NeuralNetworkEnsemblePredictor requires PyTorch. "
|
|
53
|
+
"Install the optional torch dependency to use it."
|
|
54
|
+
) from error
|
|
55
|
+
|
|
56
|
+
self.metadata = metadata
|
|
57
|
+
self._torch = torch
|
|
58
|
+
self.device = torch.device(
|
|
59
|
+
device if device is not None else "cuda" if torch.cuda.is_available() else "cpu"
|
|
60
|
+
)
|
|
61
|
+
exported_program = torch.export.load(io.BytesIO(member_model_bytes))
|
|
62
|
+
self.base_model = exported_program.module().to(self.device)
|
|
63
|
+
stacked_state = torch.load(
|
|
64
|
+
io.BytesIO(member_state_bytes), map_location=self.device, weights_only=True
|
|
65
|
+
)
|
|
66
|
+
parameter_names = {name for name, _ in self.base_model.named_parameters()}
|
|
67
|
+
buffer_names = {name for name, _ in self.base_model.named_buffers()}
|
|
68
|
+
self.params = {
|
|
69
|
+
name: value.to(self.device)
|
|
70
|
+
for name, value in stacked_state.items()
|
|
71
|
+
if name in parameter_names
|
|
72
|
+
}
|
|
73
|
+
self.buffers = {
|
|
74
|
+
name: value.to(self.device)
|
|
75
|
+
for name, value in stacked_state.items()
|
|
76
|
+
if name in buffer_names
|
|
77
|
+
}
|
|
78
|
+
self.dtype = self._infer_dtype(stacked_state)
|
|
79
|
+
self.num_input = int(metadata["num_input"])
|
|
80
|
+
|
|
81
|
+
def predict_member(
|
|
82
|
+
params: dict[str, torch.Tensor],
|
|
83
|
+
buffers: dict[str, torch.Tensor],
|
|
84
|
+
x: torch.Tensor,
|
|
85
|
+
) -> torch.Tensor:
|
|
86
|
+
return functional_call(self.base_model, (params, buffers), (x,))
|
|
87
|
+
|
|
88
|
+
self._vmap = vmap(predict_member, in_dims=(0, 0, None))
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_file(
|
|
92
|
+
cls,
|
|
93
|
+
filename: str,
|
|
94
|
+
device: Optional[Union[str, torch.device]] = None,
|
|
95
|
+
) -> "NeuralNetworkEnsemblePredictor":
|
|
96
|
+
"""Load a downloaded neural network ensemble artifact.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
filename: Path to the zip artifact written by
|
|
100
|
+
:meth:`jcmoptimizer.ActiveLearning.download_neural_network`.
|
|
101
|
+
device: PyTorch device for prediction. If omitted, CUDA is used when
|
|
102
|
+
available and CPU otherwise.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A predictor that evaluates the ensemble locally.
|
|
106
|
+
"""
|
|
107
|
+
with zipfile.ZipFile(filename, "r") as zf:
|
|
108
|
+
metadata = json.loads(zf.read("metadata.json").decode("utf-8"))
|
|
109
|
+
member_model_bytes = zf.read(metadata["member_model_file"])
|
|
110
|
+
member_state_bytes = zf.read(metadata["member_state_file"])
|
|
111
|
+
return cls(metadata, member_model_bytes, member_state_bytes, device=device)
|
|
112
|
+
|
|
113
|
+
def _infer_dtype(self, stacked_state: dict[str, torch.Tensor]) -> torch.dtype:
|
|
114
|
+
for value in stacked_state.values():
|
|
115
|
+
if self._torch.is_tensor(value) and value.is_floating_point():
|
|
116
|
+
return value.dtype
|
|
117
|
+
return self._torch.get_default_dtype()
|
|
118
|
+
|
|
119
|
+
def predict(self, x: Union[torch.Tensor, list[list[float]]]) -> torch.Tensor:
|
|
120
|
+
"""Predict for one or more input points.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
x: Input points with shape ``(num_points, num_input)``. For
|
|
124
|
+
one-dimensional input spaces, a one-dimensional sequence is also
|
|
125
|
+
accepted and interpreted as multiple points.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Tensor with shape ``(num_models, num_points, num_output)``.
|
|
129
|
+
"""
|
|
130
|
+
x_tensor = self._torch.as_tensor(x, dtype=self.dtype, device=self.device)
|
|
131
|
+
if x_tensor.ndim == 1:
|
|
132
|
+
if self.num_input != 1:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"Expected input with {self.num_input} columns, got a 1D input."
|
|
135
|
+
)
|
|
136
|
+
x_tensor = x_tensor.reshape(-1, 1)
|
|
137
|
+
if x_tensor.ndim != 2 or x_tensor.shape[1] != self.num_input:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"Expected input of shape (num_points, {self.num_input}), "
|
|
140
|
+
f"got {tuple(x_tensor.shape)}."
|
|
141
|
+
)
|
|
142
|
+
return self._vmap(self.params, self.buffers, x_tensor)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '3.1.0'
|
|
@@ -17,4 +17,6 @@ jcmoptimizer.egg-info/top_level.txt
|
|
|
17
17
|
jcmoptimizer/drivers/__init__.py
|
|
18
18
|
jcmoptimizer/drivers/active_learning_drivers.py
|
|
19
19
|
jcmoptimizer/drivers/driver.py
|
|
20
|
-
jcmoptimizer/drivers/third_party_drivers.py
|
|
20
|
+
jcmoptimizer/drivers/third_party_drivers.py
|
|
21
|
+
jcmoptimizer/utils/__init__.py
|
|
22
|
+
jcmoptimizer/utils/neural_network.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '3.0.9'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|