phaserEM 0.1__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.
Files changed (67) hide show
  1. phaser/__init__.py +0 -0
  2. phaser/__main__.py +5 -0
  3. phaser/engines/common/__init__.py +0 -0
  4. phaser/engines/common/noise_models.py +113 -0
  5. phaser/engines/common/output.py +189 -0
  6. phaser/engines/common/position_correction.py +62 -0
  7. phaser/engines/common/regularizers.py +403 -0
  8. phaser/engines/common/simulation.py +270 -0
  9. phaser/engines/conventional/__init__.py +0 -0
  10. phaser/engines/conventional/run.py +142 -0
  11. phaser/engines/conventional/solvers.py +476 -0
  12. phaser/engines/gradient/run.py +451 -0
  13. phaser/engines/gradient/solvers.py +139 -0
  14. phaser/execute.py +371 -0
  15. phaser/hooks/__init__.py +158 -0
  16. phaser/hooks/hook.py +159 -0
  17. phaser/hooks/io/empad.py +88 -0
  18. phaser/hooks/object.py +25 -0
  19. phaser/hooks/preprocessing.py +133 -0
  20. phaser/hooks/probe.py +24 -0
  21. phaser/hooks/regularization.py +97 -0
  22. phaser/hooks/scan.py +27 -0
  23. phaser/hooks/schedule.py +75 -0
  24. phaser/hooks/solver.py +169 -0
  25. phaser/io/__init__.py +0 -0
  26. phaser/io/empad.py +212 -0
  27. phaser/main.py +92 -0
  28. phaser/plan.py +184 -0
  29. phaser/py.typed +0 -0
  30. phaser/state.py +249 -0
  31. phaser/types.py +305 -0
  32. phaser/utils/__init__.py +0 -0
  33. phaser/utils/_cuda_kernels.py +213 -0
  34. phaser/utils/_jax_kernels.py +98 -0
  35. phaser/utils/analysis.py +263 -0
  36. phaser/utils/image.py +201 -0
  37. phaser/utils/io.py +402 -0
  38. phaser/utils/misc.py +295 -0
  39. phaser/utils/num.py +800 -0
  40. phaser/utils/object.py +578 -0
  41. phaser/utils/optics.py +377 -0
  42. phaser/utils/physics.py +88 -0
  43. phaser/utils/plotting.py +699 -0
  44. phaser/utils/scan.py +60 -0
  45. phaser/web/__init__.py +0 -0
  46. phaser/web/dist/03510a839ccb97b0da9f.module.wasm +0 -0
  47. phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
  48. phaser/web/dist/bundle-dashboard.js +712 -0
  49. phaser/web/dist/bundle-manager.js +210 -0
  50. phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +106 -0
  51. phaser/web/dist/style.css +152 -0
  52. phaser/web/notebook.py +269 -0
  53. phaser/web/routes.py +211 -0
  54. phaser/web/server.py +540 -0
  55. phaser/web/slurm.py +180 -0
  56. phaser/web/templates/base.html +13 -0
  57. phaser/web/templates/dashboard.html +14 -0
  58. phaser/web/templates/manager.html +19 -0
  59. phaser/web/types.py +255 -0
  60. phaser/web/util.py +116 -0
  61. phaser/web/worker.py +200 -0
  62. phaserem-0.1.dist-info/METADATA +121 -0
  63. phaserem-0.1.dist-info/RECORD +67 -0
  64. phaserem-0.1.dist-info/WHEEL +5 -0
  65. phaserem-0.1.dist-info/entry_points.txt +2 -0
  66. phaserem-0.1.dist-info/licenses/LICENSE.txt +373 -0
  67. phaserem-0.1.dist-info/top_level.txt +1 -0
phaser/web/slurm.py ADDED
@@ -0,0 +1,180 @@
1
+ import asyncio
2
+ import logging
3
+ import weakref
4
+ import json
5
+ import os
6
+ import re
7
+ import importlib.resources
8
+ import typing as t
9
+
10
+ from .server import WorkerID, Worker
11
+
12
+
13
+ SlurmID: t.TypeAlias = int
14
+ SlurmState: t.TypeAlias = t.Literal[
15
+ 'BOOT_FAIL', 'CANCELLED', 'COMPLETED', 'CONFIGURING', 'COMPLETING',
16
+ 'DEADLINE', 'FAILED', 'NODE_FAIL', 'OUT_OF_MEMORY', 'PENDING',
17
+ 'PREEMPTED', 'RUNNING', 'RESV_DEL_HOLD', 'REQUEUE_FED', 'REQUEUE_HOLD',
18
+ 'REQUEUED', 'RESIZING', 'REVOKED', 'SIGNALING', 'SPECIAL_EXIT',
19
+ 'STAGE_OUT', 'STOPPED', 'SUSPENDED', 'TIMEOUT',
20
+ ]
21
+
22
+
23
+ class SlurmJobInfo(t.TypedDict):
24
+ job_id: SlurmID
25
+ job_state: SlurmState
26
+ name: str
27
+ nodes: str
28
+ user_name: str
29
+
30
+ # path to stdout and stderr
31
+ standard_output: str
32
+ standard_error: str
33
+
34
+ submit_time: int
35
+ start_time: int
36
+ eligible_time: int
37
+ end_time: int
38
+
39
+
40
+ class SqueueResult(t.TypedDict):
41
+ meta: t.Dict[str, t.Any]
42
+ jobs: t.List[SlurmJobInfo]
43
+ warnings: t.List[t.Any]
44
+ errors: t.List[t.Any]
45
+
46
+
47
+ class SlurmWorker(Worker):
48
+ def __init__(self, worker_id: WorkerID, slurm_job_id: SlurmID):
49
+ self.slurm_job_id: SlurmID = slurm_job_id
50
+ super().__init__(worker_id)
51
+
52
+ async def cancel(self):
53
+ from .server import server
54
+ if self.status == 'queued':
55
+ await server.slurm_manager.cancel_queued_worker(self.slurm_job_id)
56
+
57
+ await self.set_status('stopping')
58
+
59
+
60
+ class SlurmManager:
61
+ def __init__(self):
62
+ self.logger = logging.getLogger(__name__)
63
+ self._slurm_exists: t.Optional[bool] = None
64
+ self._slurm_workers: weakref.WeakValueDictionary[SlurmID, SlurmWorker] = weakref.WeakValueDictionary()
65
+
66
+ self._poll_task: t.Optional[asyncio.Task[None]] = None
67
+ self._new_worker_event: asyncio.Event = asyncio.Event()
68
+
69
+ async def make_worker(self, worker_id: WorkerID, url: str) -> SlurmWorker:
70
+ await self.check_slurm_exists()
71
+
72
+ job_name = f"phaser_{worker_id}"
73
+
74
+ from .. import web
75
+ with importlib.resources.path(web, 'slurm_worker.sh') as script_path:
76
+ slurm_args = "--qos=high --time=4-0 --partition=xeon-g6-volta --cpus-per-task=20 --gres=gpu:volta:1 --signal=SIGINT@120"
77
+ #slurm_args = "--qos=high --time=01:00:00 --partition=debug-cpu --cpus-per-task=20"
78
+ shell = os.environ.get('SHELL', '/bin/bash')
79
+ proc = await asyncio.create_subprocess_exec(
80
+ shell, "-c", f"sbatch --job-name={job_name} {slurm_args} \"{script_path.absolute()}\" \"{url}\"",
81
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
82
+ )
83
+ stdout, stderr = await proc.communicate()
84
+
85
+ if proc.returncode != 0:
86
+ raise RuntimeError(f"Failed to start slurm worker, stderr: '{stderr}'")
87
+
88
+ try:
89
+ match = re.fullmatch(r'Submitted batch job (\d+)', stdout.decode().strip())
90
+ job_id: SlurmID = int(match[1]) # type: ignore
91
+ except (ValueError, TypeError):
92
+ raise ValueError(f"Failed to parse slurm return: '{stdout.decode()}'")
93
+
94
+ worker = SlurmWorker(worker_id, job_id)
95
+ self._slurm_workers[job_id] = worker
96
+
97
+ if self._poll_task is None:
98
+ self._poll_task = asyncio.create_task(self._poll_slurm())
99
+ else:
100
+ self._new_worker_event.set()
101
+
102
+ return worker
103
+
104
+ async def cancel_queued_worker(self, slurm_job_id: SlurmID):
105
+ proc = await asyncio.create_subprocess_shell(
106
+ f"scancel --state=PENDING {slurm_job_id}",
107
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
108
+ )
109
+ stdout, stderr = await proc.communicate()
110
+
111
+ if proc.returncode != 0:
112
+ self.logger.warning(f"Failed to cancel slurm job: '{stderr.decode()}'")
113
+
114
+ async def _poll_slurm(self):
115
+ while True:
116
+ while True:
117
+ try:
118
+ await self._poll_slurm_status()
119
+ except Exception as e:
120
+ self.logger.warning(f"Failed to poll slurm worker statuses: {e}")
121
+
122
+ if any(worker.state == 'queued' for worker in self._slurm_workers.values()):
123
+ # fast wait cycle
124
+ await asyncio.sleep(5.0)
125
+ elif any(worker.state != 'stopped' for worker in self._slurm_workers.values()):
126
+ # if there's any workers to wait for, slow wait cycle
127
+ await asyncio.sleep(30.0)
128
+ else:
129
+ # no workers waiting, wait until one is started
130
+ self._new_worker_event.clear()
131
+ break
132
+ await self._new_worker_event.wait()
133
+
134
+ async def _poll_slurm_status(self):
135
+ jobs = ",".join(str(worker.slurm_job_id) for worker in self._slurm_workers.values())
136
+
137
+ proc = await asyncio.create_subprocess_shell(
138
+ f"squeue --job={jobs} --states=all --json",
139
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
140
+ )
141
+ stdout, stderr = await proc.communicate()
142
+
143
+ if proc.returncode != 0:
144
+ self.logger.warning(f"Failed to poll slurm worker statuses, stderr: '{stderr}'")
145
+ return
146
+
147
+ d = t.cast(SqueueResult, json.loads(stdout))
148
+
149
+ for job in d["jobs"]:
150
+ try:
151
+ worker = self._slurm_workers[job["job_id"]]
152
+ except KeyError:
153
+ continue
154
+
155
+ if job['job_state'] in ('CONFIGURING', 'RUNNING'):
156
+ if worker.status == 'queued':
157
+ await worker.set_status('starting')
158
+ elif job['job_state'] != 'PENDING':
159
+ if worker.status != 'stopped':
160
+ # TODO grab some exit information here
161
+ await worker.set_status('stopped')
162
+
163
+ async def check_slurm_exists(self):
164
+ if self._slurm_exists is None:
165
+ proc = await asyncio.create_subprocess_shell("sbatch --version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
166
+ stdout, stderr = await proc.communicate()
167
+
168
+ if proc.returncode == 0:
169
+ self._slurm_exists = True
170
+ self.logger.info(f"Slurm found, version '{stdout.decode().strip()}'")
171
+ else:
172
+ self._slurm_exists = False
173
+ self.logger.warning(f"Slurm not found, stderr '{stderr.decode()}'")
174
+
175
+ if not self._slurm_exists:
176
+ raise RuntimeError("Slurm not found, is it installed and on PATH?")
177
+
178
+ async def finalize(self):
179
+ if self._poll_task is not None:
180
+ self._poll_task.cancel()
@@ -0,0 +1,13 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>{% block title %}Phaser{% endblock %}</title>
6
+ <link href="{{ url_for('static', filename='style.css') }}" rel='stylesheet'>
7
+ {% block head %}{% endblock %}
8
+ </head>
9
+ <body>
10
+ {% block body %}{% endblock %}
11
+ <noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
12
+ </body>
13
+ </html>
@@ -0,0 +1,14 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}Phaser frontend{% endblock %}
3
+
4
+ {% block head %}
5
+ <script type="text/javascript" src="{{ url_for('static', filename='bundle-dashboard.js') }}" defer></script>
6
+ {% endblock %}
7
+
8
+ {% block body %}
9
+ <div id="app"></div>
10
+ <!--<div class="section-header">Probe</div>
11
+ <div class="section">
12
+ <div style="width: 100%; margin: auto;" class="plot-cont" id="probe-plot"></div>
13
+ </div>-->
14
+ {% endblock %}
@@ -0,0 +1,19 @@
1
+ {% extends "base.html" %}
2
+ {% block title %}Phaser frontend{% endblock %}
3
+
4
+ {% block head %}
5
+ <script type="text/javascript" src="{{ url_for('static', filename='bundle-manager.js') }}" defer></script>
6
+ {% endblock %}
7
+
8
+ {% block body %}
9
+ <div id="app"></div>
10
+ <!--<div class="section-header">Start new reconstruction</div>
11
+ <div class="section">
12
+ <button id="start">Start reconstruction</button>
13
+ <button id="start-fake">Start fake reconstruction</button>
14
+ </div>
15
+ <div class="section-header">Current reconstructions</div>
16
+ <div class="section">
17
+ <table id="recons-table"></table>
18
+ </div>-->
19
+ {% endblock %}
phaser/web/types.py ADDED
@@ -0,0 +1,255 @@
1
+ import datetime
2
+ import logging
3
+ import typing as t
4
+
5
+ import pane
6
+ from pane.annotations import Tagged
7
+ from typing_extensions import Self
8
+
9
+ from phaser.types import Cancelled
10
+ from .util import ReconsStateConverter
11
+
12
+
13
+ JobID: t.TypeAlias = str
14
+ WorkerID: t.TypeAlias = str
15
+
16
+ WorkerStatus: t.TypeAlias = t.Literal['queued', 'starting', 'reloading', 'idle', 'running', 'stopping', 'stopped', 'unknown']
17
+ """
18
+ Enum indicating the state of the connection, as described by either the server or client.
19
+
20
+ - 'queued': Worker is queued/scheduled to start
21
+ - 'starting': Worker is actively being started
22
+ - 'reloading': Worker is being reloaded
23
+ - 'idle': No job is running
24
+ - 'running': Job is running
25
+ - 'stopping': Worker is being stopped
26
+ - 'stopped': Worker has been stopped (normally or abnormally)
27
+
28
+ - 'unknown': Job hasn't talked in a while, unknown state
29
+ """
30
+
31
+ JobStatus: t.TypeAlias = t.Literal['queued', 'starting', 'running', 'stopping', 'stopped']
32
+ """
33
+ Enum indicating the state of a reconstruction job
34
+ """
35
+
36
+ Result: t.TypeAlias = t.Literal['finished', 'errored', 'cancelled', 'interrupted']
37
+ """
38
+ Enum indicating a job or worker result.
39
+
40
+ - 'finished': Finished normally
41
+ - 'errored': Encounted an error running
42
+ - 'cancelled': Cancelled/shutdown by the user (from server side)
43
+ - 'interrupted': Cancelled/shutdown by the client/OS
44
+ """
45
+
46
+ Signal: t.TypeAlias = t.Literal['shutdown', 'cancel', 'reload']
47
+ """
48
+ Signal sent to a job or worker.
49
+ """
50
+
51
+ class ValidationError(Exception):
52
+ def __init__(self, msg: str):
53
+ self.msg: str = msg
54
+
55
+ class SignalException(Cancelled):
56
+ def __init__(self, signal: Signal, urgent: bool = False):
57
+ self.signal: Signal = signal
58
+ self.urgent: bool = urgent
59
+
60
+ # server -> client messages
61
+
62
+ class WorkerState(pane.PaneBase):
63
+ worker_id: WorkerID
64
+ status: WorkerStatus
65
+ links: t.Dict[str, str] = pane.field(default_factory=dict)
66
+
67
+ class WorkerUpdate(pane.PaneBase):
68
+ worker_id: WorkerID
69
+ status: WorkerStatus
70
+
71
+ msg: t.Literal['status_change'] = 'status_change'
72
+
73
+ class JobState(pane.PaneBase):
74
+ job_id: JobID
75
+ status: JobStatus
76
+ links: t.Dict[str, str] = pane.field(default_factory=dict)
77
+ state: t.Dict[str, t.Any] = pane.field(converter=ReconsStateConverter(), default_factory=dict)
78
+ worker_id: t.Optional[WorkerID] = None
79
+
80
+ def strip_state(self) -> Self:
81
+ return type(self)(
82
+ self.job_id, self.status, self.links
83
+ )
84
+
85
+ class LogRecord(pane.PaneBase):
86
+ i: int
87
+
88
+ timestamp: datetime.datetime
89
+
90
+ log: str
91
+
92
+ logger_name: str
93
+ log_level: int
94
+
95
+ line_number: int
96
+ func_name: t.Optional[str] = None
97
+ stack_info: t.Optional[str] = None
98
+
99
+ class JobStatusChange(pane.PaneBase):
100
+ status: JobStatus
101
+ job_id: JobID
102
+
103
+ msg: t.Literal['status_change'] = 'status_change'
104
+
105
+ class JobUpdate(pane.PaneBase):
106
+ state: t.Dict[str, t.Any] = pane.field(converter=ReconsStateConverter())
107
+ job_id: JobID
108
+
109
+ msg: t.Literal['job_update'] = 'job_update'
110
+
111
+ class LogUpdate(pane.PaneBase):
112
+ new_logs: t.List[LogRecord]
113
+
114
+ msg: t.Literal['log'] = 'log'
115
+
116
+ class JobStopped(pane.PaneBase):
117
+ result: Result
118
+ error: t.Optional[str] = None
119
+
120
+ msg: t.Literal['job_stopped'] = 'job_stopped'
121
+
122
+ JobMessage: t.TypeAlias = t.Annotated[t.Union[
123
+ JobStatusChange, JobUpdate, LogUpdate, JobStopped
124
+ ], Tagged('msg')]
125
+
126
+ class DashboardConnected(pane.PaneBase):
127
+ state: JobState
128
+ msg: t.Literal['connected'] = 'connected'
129
+
130
+ DashboardMessage: t.TypeAlias = t.Annotated[t.Union[
131
+ JobStatusChange, JobUpdate, JobStopped, DashboardConnected
132
+ ], Tagged('msg')]
133
+
134
+ class WorkersUpdate(pane.PaneBase):
135
+ event: t.Optional[WorkerUpdate]
136
+ state: t.List[WorkerState]
137
+
138
+ msg: t.Literal['workers_update'] = 'workers_update'
139
+
140
+ class JobsUpdate(pane.PaneBase):
141
+ event: JobMessage
142
+ state: t.List[JobState]
143
+
144
+ msg: t.Literal['jobs_update'] = 'jobs_update'
145
+
146
+ class ManagerConnected(pane.PaneBase):
147
+ workers: t.List[WorkerState]
148
+ jobs: t.List[JobState]
149
+
150
+ msg: t.Literal['connected'] = 'connected'
151
+
152
+ ManagerMessage: t.TypeAlias = t.Annotated[t.Union[
153
+ JobsUpdate, WorkersUpdate, ManagerConnected
154
+ ], Tagged('msg')]
155
+
156
+ # worker -> server messages
157
+
158
+ class ConnectMessage(pane.PaneBase):
159
+ """Message sent when a worker starts up"""
160
+ msg: t.Literal['connect'] = 'connect'
161
+
162
+ class PollMessage(pane.PaneBase):
163
+ """Message polling the server for a job"""
164
+ msg: t.Literal['poll'] = 'poll'
165
+
166
+ class PingMessage(pane.PaneBase):
167
+ """Message pinging the server, potentially receiving a cancellation"""
168
+ msg: t.Literal['ping'] = 'ping'
169
+
170
+ class UpdateMessage(pane.PaneBase):
171
+ """Message containing some state update"""
172
+ state: t.Dict[str, t.Any] = pane.field(converter=ReconsStateConverter())
173
+ job_id: JobID
174
+ msg: t.Literal['job_update'] = 'job_update'
175
+
176
+ class LogMessage(pane.PaneBase):
177
+ """Message corresponding to a log entry"""
178
+ job_id: t.Optional[JobID]
179
+
180
+ timestamp: datetime.datetime
181
+
182
+ log: str
183
+
184
+ logger_name: str
185
+ log_level: int
186
+
187
+ line_number: int
188
+ func_name: t.Optional[str] = None
189
+ stack_info: t.Optional[str] = None
190
+
191
+ msg: t.Literal['log'] = 'log'
192
+
193
+ @classmethod
194
+ def from_logrecord(cls, job_id: t.Optional[JobID], record: logging.LogRecord) -> Self:
195
+ timestamp = datetime.datetime.fromtimestamp(record.created)
196
+ return cls(
197
+ timestamp=timestamp,
198
+ log=record.getMessage(),
199
+ job_id=job_id,
200
+ logger_name=record.name,
201
+ log_level=record.levelno,
202
+ line_number=record.lineno,
203
+ func_name=record.funcName,
204
+ stack_info=record.stack_info,
205
+ )
206
+
207
+ def into_record(self, i: int) -> LogRecord:
208
+ return LogRecord.make_unchecked(
209
+ i, self.timestamp, self.log, self.logger_name, self.log_level,
210
+ self.line_number, self.func_name, self.stack_info
211
+ )
212
+
213
+ class JobResultMessage(pane.PaneBase):
214
+ """Message indicating job has stopped (normally or abnormally)"""
215
+ job_id: JobID
216
+ result: Result
217
+ error: t.Optional[str] = None
218
+
219
+ msg: t.Literal['job_result'] = 'job_result'
220
+
221
+ class WorkerShutdownMessage(pane.PaneBase):
222
+ """Message indicating a worker has been stopped"""
223
+ result: Result
224
+ error: t.Optional[str] = None
225
+ detail: t.Optional[str] = None
226
+
227
+ msg: t.Literal['shutdown'] = 'shutdown'
228
+
229
+
230
+ # server -> worker responses
231
+
232
+ class JobResponse(pane.PaneBase):
233
+ """Response containing a job to run"""
234
+ job_id: JobID
235
+ plan: str
236
+ msg: t.Literal['job'] = 'job'
237
+
238
+ class OkResponse(pane.PaneBase):
239
+ """Response from server indicating no action should be taken"""
240
+ msg: t.Literal['ok'] = 'ok'
241
+
242
+ class SignalResponse(pane.PaneBase):
243
+ """Response from server signalling some action (cancellation, shutdown, or reload)"""
244
+ signal: t.Literal['shutdown', 'cancel', 'reload'] = 'cancel'
245
+ urgent: bool = False
246
+ msg: t.Literal['signal'] = 'signal'
247
+
248
+ WorkerMessage: t.TypeAlias = t.Annotated[t.Union[
249
+ PollMessage, ConnectMessage, PingMessage, UpdateMessage,
250
+ LogMessage, JobResultMessage, WorkerShutdownMessage,
251
+ ], Tagged('msg')]
252
+
253
+ ServerResponse: t.TypeAlias = t.Annotated[t.Union[
254
+ JobResponse, SignalResponse, OkResponse
255
+ ], Tagged('msg')]
phaser/web/util.py ADDED
@@ -0,0 +1,116 @@
1
+ import asyncio
2
+ import base64
3
+ import dataclasses
4
+ import typing as t
5
+
6
+ import numpy
7
+ from pane.converters import Converter
8
+ from pane.errors import ProductErrorNode, WrongTypeError, ParseInterrupt
9
+
10
+ T = t.TypeVar('T')
11
+
12
+ class _array_dummy():
13
+ def __init__(self, array_interface: t.Any):
14
+ self.__array_interface__ = array_interface
15
+
16
+
17
+ async def merge_streams(
18
+ *its: t.AsyncIterable[T]
19
+ ) -> t.AsyncIterator[T]:
20
+ # TODO: better error handling here
21
+ queue = asyncio.Queue(1)
22
+
23
+ async def task(it: t.AsyncIterable[T]):
24
+ async for item in it:
25
+ await queue.put(item)
26
+
27
+ async with asyncio.TaskGroup() as tg:
28
+ tasks = [tg.create_task(task(it)) for it in its]
29
+
30
+ while not all(task.done() for task in tasks):
31
+ yield await queue.get()
32
+
33
+
34
+ def encode_obj(obj: t.Any, to_numpy: bool = True) -> t.Any:
35
+ if isinstance(obj, numpy.ndarray):
36
+ if not to_numpy:
37
+ return obj.tolist()
38
+ d = obj.__array_interface__
39
+ d['data'] = base64.urlsafe_b64encode(obj.tobytes()).decode('ascii')
40
+ d['_ty'] = 'numpy'
41
+ return d
42
+
43
+ if isinstance(obj, bytes):
44
+ return {
45
+ '_ty': 'bytes',
46
+ 'data': base64.urlsafe_b64encode(obj).decode('ascii')
47
+ }
48
+
49
+ if isinstance(obj, dict):
50
+ d = obj
51
+ elif dataclasses.is_dataclass(obj):
52
+ d = obj.asdict() # type: ignore
53
+ else:
54
+ return obj
55
+
56
+ return {k: encode_obj(v, to_numpy and k not in ('sampling',)) for (k, v) in d.items()}
57
+
58
+
59
+ def decode_obj(obj: t.Any) -> t.Any:
60
+ if isinstance(obj, dict):
61
+ d = obj
62
+ elif dataclasses.is_dataclass(obj):
63
+ d = obj.asdict() # type: ignore
64
+ else:
65
+ return obj
66
+
67
+ if '_ty' not in d:
68
+ return {k: decode_obj(v) for (k, v) in d.items()}
69
+ ty = d.pop('_ty')
70
+
71
+ if ty == 'numpy':
72
+ d['data'] = base64.urlsafe_b64decode(d['data'].encode('utf-8'))
73
+ d['shape'] = tuple(d['shape'])
74
+ return numpy.array(_array_dummy(d))
75
+
76
+ if ty == 'bytes':
77
+ return base64.urlsafe_b64decode(d['data'].encode('utf-8'))
78
+
79
+ raise ValueError(f"Unknown custom type '{ty}', while parsing reconstruction state update")
80
+
81
+
82
+ class ReconsStateConverter(Converter[t.Dict[str, t.Any]]):
83
+ def __init__(self):
84
+ ...
85
+
86
+ def expected(self, plural: bool = False) -> str:
87
+ return "reconstruction state"
88
+
89
+ def into_data(self, val: t.Any) -> t.Any:
90
+ return encode_obj(val)
91
+
92
+ def try_convert(self, val: t.Any) -> t.Dict[str, t.Any]:
93
+ try:
94
+ val = decode_obj(val)
95
+ except Exception:
96
+ raise ParseInterrupt()
97
+ #if not isinstance(val, t.Mapping) or ({'iter', 'wavelength'} - val.keys()):
98
+ # raise ParseInterrupt()
99
+
100
+ return val # type: ignore
101
+
102
+ def collect_errors(self, val: t.Any) -> t.Union[ProductErrorNode, WrongTypeError, None]:
103
+ try:
104
+ val = decode_obj(val)
105
+ except Exception as e:
106
+ import traceback
107
+
108
+ tb = e.__traceback__.tb_next # type: ignore
109
+ tb = traceback.TracebackException(type(e), e, tb)
110
+ return WrongTypeError(self.expected(), val, tb)
111
+ if not isinstance(val, t.Mapping):
112
+ return WrongTypeError(self.expected(), val)
113
+ #if (missing := {'iter', 'wavelength'} - val.keys()):
114
+ # return ProductErrorNode(self.expected(), {}, val, missing)
115
+
116
+ return None