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/io/empad.py ADDED
@@ -0,0 +1,212 @@
1
+
2
+ from pathlib import Path
3
+ import re
4
+ import typing as t
5
+
6
+ import numpy
7
+ import pane
8
+ import pane.io
9
+ from numpy.typing import NDArray
10
+ from pane.annotations import shape
11
+ from pane.convert import IntoConverterHandlers
12
+ from typing_extensions import Self
13
+
14
+ from phaser.types import IsVersion
15
+
16
+
17
+ def _get_dir(f: pane.io.FileOrPath) -> t.Optional[Path]:
18
+ if isinstance(f, (str, Path)):
19
+ return Path(f).parent
20
+
21
+ name = getattr(f, 'name', None)
22
+ if name in (None, '<stdout>', '<stderr>'):
23
+ return None
24
+ path = Path(name)
25
+ return path.parent if path.exists() else None
26
+
27
+
28
+ class EmpadMetadata(pane.PaneBase, frozen=False, kw_only=True, allow_extra=True):
29
+ file_type: t.Literal['pyMultislicer_metadata', 'empad_metadata'] = 'empad_metadata'
30
+
31
+ @classmethod
32
+ def from_json(cls, f: pane.io.FileOrPath, *,
33
+ custom: t.Optional[IntoConverterHandlers] = None) -> Self:
34
+ path = _get_dir(f)
35
+ self = pane.io.from_json(f, cls, custom=custom)
36
+ object.__setattr__(self, 'path', path)
37
+ return self
38
+
39
+ def __post_init__(self):
40
+ object.__setattr__(self, 'path', None)
41
+
42
+ name: str
43
+ """Experiment name"""
44
+
45
+ version: t.Annotated[str, IsVersion(exactly="2.0")] = "2.0"
46
+ """Metadata version"""
47
+
48
+ raw_filename: str
49
+ """Raw 4DSTEM data filename, relative to metadata location."""
50
+
51
+ orig_path: t.Optional[Path] = None
52
+ """Original path to experimental folder."""
53
+
54
+ path: t.Optional[Path] = pane.field(init=False, exclude=True)
55
+ """Current path to experimental folder (based on metadata loading)"""
56
+
57
+ author: t.Optional[str] = None
58
+ """Author of dataset"""
59
+ time: t.Optional[str] = None
60
+ """Image acquisition time (RFC 2822 format)"""
61
+ time_unix: t.Optional[float] = None
62
+ """Image acquisition time (seconds since Unix epoch)"""
63
+ bg_unix: t.Optional[float] = None
64
+ """Background image acquisition time (seconds since Unix epoch)"""
65
+ has_bg: t.Optional[bool] = None
66
+ """Whether background image is valid"""
67
+
68
+ voltage: float
69
+ """Accelerating voltage (V)."""
70
+ conv_angle: t.Optional[float] = None
71
+ """Convergence angle (mrad)."""
72
+ defocus: t.Optional[float] = None
73
+ """Defocus (m). Positive is overfocus."""
74
+ camera_length: t.Optional[float] = None
75
+ """Camera length (m)."""
76
+ diff_step: t.Optional[float] = None
77
+ """Diffraction pixel size (mrad/px)."""
78
+
79
+ scan_rotation: float
80
+ """Scan rotation (degrees)."""
81
+ scan_shape: t.Tuple[int, int]
82
+ """Scan shape (x, y)."""
83
+ scan_fov: t.Tuple[float, float]
84
+ """Scan field of view (m)."""
85
+ scan_step: t.Tuple[float, float]
86
+ """Scan step (m/px)."""
87
+
88
+ exposure_time: t.Optional[float] = None
89
+ """Pixel exposure time (s)."""
90
+ post_exposure_time: t.Optional[float] = None
91
+ """Pixel post-exposure time (s)."""
92
+ beam_current: t.Optional[float] = None
93
+ """Approx. beam current (A)."""
94
+ adu: t.Optional[float] = None
95
+ """Single-electron intensity (data units)."""
96
+
97
+ scan_correction: t.Optional[t.Annotated[NDArray[numpy.floating], shape((2, 2))]] = None
98
+ """Scan correction matrix, [x', y'] = scan_correction @ [x, y]"""
99
+
100
+ scan_positions: t.Optional[t.List[t.Tuple[float, float]]] = None
101
+ """
102
+ Scan position override (m).
103
+ Should be specified as a 1d list of (x, y) positions, in scan order. `scan_correction` is applied to these positions (if present).
104
+ """
105
+
106
+ notes: t.Optional[str] = None
107
+
108
+ crop: t.Optional[t.Tuple[int, int, int, int]] = None
109
+ """Region scan is valid within, (min_y, max_y, min_x, max_x). Python-style slicing."""
110
+
111
+ def is_simulated(self) -> bool:
112
+ return self.file_type == "pyMultislicer_metadata"
113
+
114
+
115
+ def load_4d(path: t.Union[str, Path], scan_shape: t.Optional[t.Tuple[int, int]] = None,
116
+ memmap: bool = False) -> NDArray[numpy.float32]:
117
+ """
118
+ Load a raw EMPAD dataset into memory.
119
+
120
+ The file is loaded so the dimensions are: (scan_y, scan_x, k_y, k_x), with y decreasing downwards.
121
+
122
+ Patterns are not fftshifted or normalized upon loading.
123
+
124
+ # Parameters
125
+
126
+ - `path`: Path to file to load
127
+ - `scan_shape`: Scan shape of dataset. Will be inferred from the filename if not specified.
128
+ - `memmap`: If specified, memmap the file as opposed to loading it eagerly.
129
+
130
+ Returns a numpy array (or `numpy.memmap`)
131
+ """
132
+ path = Path(path)
133
+
134
+ if scan_shape is None:
135
+ match = re.search(r"x(\d+)_y(\d+)", path.name)
136
+ if match:
137
+ n_x, n_y = map(int, (match[1], match[2]))
138
+ else:
139
+ raise ValueError(f"Unable to infer probe dimensions from name {path.name}")
140
+ else:
141
+ n_y, n_x = scan_shape
142
+
143
+ if memmap:
144
+ a = numpy.memmap(path, dtype=numpy.float32, mode='r')
145
+ else:
146
+ a = numpy.fromfile(path, dtype=numpy.float32)
147
+
148
+ if not a.size % (130*128) == 0:
149
+ raise ValueError(f"File not divisible by 130x128 (size={a.size}).")
150
+ a.shape = (-1, 130, 128)
151
+ #a = a[:, :128, :]
152
+
153
+ if a.shape[0] != n_x * n_y:
154
+ raise ValueError(f"Got {a.shape[0]} probes, expected {n_x}x{n_y} = {n_x * n_y}.")
155
+ a.shape = (n_y, n_x, *a.shape[1:])
156
+ a = a[..., 127::-1, :] # flip reciprocal y space, crop junk rows
157
+
158
+ return a
159
+
160
+
161
+ @t.overload
162
+ def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path], folder: None = None, name: None = None):
163
+ ...
164
+
165
+ @t.overload
166
+ def save_4d(arr: NDArray[numpy.float32], *, path: None = None, folder: t.Union[str, Path], name: t.Optional[str] = None):
167
+ ...
168
+
169
+ def save_4d(arr: NDArray[numpy.float32], *, path: t.Union[str, Path, None] = None,
170
+ folder: t.Union[str, Path, None] = None, name: t.Optional[str] = None): #):
171
+ """
172
+ Save a raw EMPAD dataset.
173
+
174
+ Either `path` or `folder` can be specified. If `folder` is specified,
175
+ `name` will be used as a format string to determine the filename.
176
+ `path` and `folder` cannot be specified simultaneously.
177
+
178
+ Patterns are not fftshifted or normalized upon saving.
179
+
180
+ Parameters:
181
+ - `arr`: Array to save
182
+ - `path`: Path to save dataset to.
183
+ - `folder`: Folder to save dataset inside.
184
+ - `name`: When `folder` is specified, format to use to determine filename. Defaults to `"scan_x{x}_y{y}.raw"`.
185
+ Will be formatted using the scan shape `{'x': n_x, 'y': n_y}`.
186
+ """
187
+
188
+ try:
189
+ assert len(arr.shape) == 4
190
+ assert arr.shape[2:] == (128, 128)
191
+ except AssertionError as e:
192
+ raise ValueError("Invalid data format") from e
193
+
194
+ if folder is not None:
195
+ if path is not None:
196
+ raise ValueError("Cannot specify both 'path' and 'folder'")
197
+
198
+ n_y, n_x = arr.shape[:2]
199
+ path = Path(folder) / (name or "scan_x{x}_y{y}.raw").format(x=n_x, y=n_y)
200
+ elif path is not None:
201
+ path = Path(path)
202
+ else:
203
+ raise ValueError("Must specify either 'path' or 'folder'")
204
+
205
+ out_shape = list(arr.shape)
206
+ out_shape[2] = 130 # dead rows
207
+
208
+ out = numpy.zeros(out_shape, dtype=numpy.float32)
209
+ out[..., 127::-1, :] = arr.astype(numpy.float32)
210
+
211
+ with open(path, 'wb') as f:
212
+ out.tofile(f)
phaser/main.py ADDED
@@ -0,0 +1,92 @@
1
+ from pathlib import Path
2
+ import sys
3
+ import typing as t
4
+
5
+ import click
6
+
7
+ @click.group()
8
+ def cli():
9
+ pass
10
+
11
+
12
+ @cli.command('run')
13
+ @click.argument('path', type=click.Path(exists=True, dir_okay=False))
14
+ def run(path: t.Union[str, Path]):
15
+ from .plan import ReconsPlan
16
+ from .execute import execute_plan
17
+ plans = ReconsPlan.from_yaml_all(path)
18
+
19
+ for plan in plans:
20
+ execute_plan(plan)
21
+
22
+
23
+ @cli.command('serve')
24
+ @click.option('--host', type=str, default='localhost')
25
+ @click.option('--port', type=int)
26
+ @click.option('-v', '--verbose', count=True)
27
+ def serve(host: str = 'localhost', port: t.Optional[int] = None, verbose: int = 0):
28
+ from phaser.web.server import server
29
+
30
+ if ':' in host:
31
+ (host, port_from_host) = host.rsplit(':', maxsplit=1)
32
+ try:
33
+ port_from_host = int(port_from_host)
34
+ except ValueError:
35
+ print(f"Invalid host '{host}:{port_from_host}'", file=sys.stderr)
36
+ sys.exit(1)
37
+
38
+ port = port or port_from_host
39
+
40
+ server.run(hostname=host, port=port, verbosity=verbose)
41
+
42
+
43
+ @cli.command('validate')
44
+ @click.argument('path', type=click.Path(allow_dash=True), default='-')
45
+ @click.option('--json/--no-json', default=False)
46
+ def validate(path: t.Union[str, Path], json: bool = False):
47
+ from contextlib import nullcontext
48
+ from .plan import ReconsPlan
49
+
50
+ try:
51
+ if path == '-':
52
+ file = nullcontext(sys.stdin)
53
+ else:
54
+ file = open(Path(path).expanduser(), 'r')
55
+
56
+ with file as file:
57
+ plans = ReconsPlan.from_yaml_all(file)
58
+ except Exception as e:
59
+ print(f"Validation failed:\n{e}", file=sys.stderr)
60
+
61
+ if json:
62
+ from json import dump
63
+ dump({'result': 'error', 'error': str(e)}, sys.stdout)
64
+ print()
65
+
66
+ sys.exit(1)
67
+
68
+ if len(plans) == 1:
69
+ print("Validation of plan successful!", file=sys.stderr)
70
+ else:
71
+ print(f"Validation of {len(plans)} plans successful!", file=sys.stderr)
72
+
73
+ if json:
74
+ from json import dump
75
+ dump({
76
+ 'result': 'success',
77
+ 'plans': [plan.into_data() for plan in plans],
78
+ }, sys.stdout)
79
+ print()
80
+
81
+
82
+ @cli.command('worker')
83
+ @click.argument('url', type=str, required=True)
84
+ @click.option('--quiet/--loud', default=False)
85
+ def worker(url: str, quiet: bool = False):
86
+ from phaser.web.worker import run_worker
87
+
88
+ run_worker(url, quiet=quiet)
89
+
90
+
91
+ if __name__ == '__main__':
92
+ cli()
phaser/plan.py ADDED
@@ -0,0 +1,184 @@
1
+ from pathlib import Path
2
+ import typing as t
3
+
4
+ from .types import Dataclass, Slices, BackendName, Flag, ReconsVars, IsVersion, EmptyDict
5
+ from .hooks import RawDataHook, ProbeHook, ObjectHook, ScanHook, EngineHook, PostInitHook, PostLoadHook
6
+ from .hooks.solver import NoiseModelHook, ConventionalSolverHook, PositionSolverHook, GradientSolverHook
7
+ from .hooks.schedule import FlagLike, ScheduleLike
8
+ from .hooks.regularization import IterConstraintHook, GroupConstraintHook, CostRegularizerHook
9
+
10
+
11
+ SaveType: t.TypeAlias = t.Literal[
12
+ 'probe', 'probe_mag', 'probe_recip', 'probe_recip_mag',
13
+ 'object_phase_stack', 'object_phase_sum',
14
+ 'object_mag_stack', 'object_mag_sum',
15
+ ]
16
+
17
+
18
+ class InitPlan(Dataclass, kw_only=True):
19
+ state: t.Optional[Path] = None
20
+
21
+ scan: t.Union[EmptyDict, ScanHook, None] = None
22
+ probe: t.Union[EmptyDict, ProbeHook, None] = None
23
+ object: t.Optional[ObjectHook] = None # ObjectHook('random')
24
+
25
+
26
+ class SaveOptions(Dataclass, kw_only=True):
27
+ images: t.Tuple[SaveType, ...] = ('probe', 'object_phase_stack')
28
+ crop_roi: bool = True
29
+ unwrap_phase: bool = True
30
+ img_dtype: t.Literal['float', '8bit', '16bit', '32bit'] = '16bit'
31
+
32
+ out_dir: str = "{name}"
33
+ img_fmt: str = "{type}_iter{iter.total_iter}.tiff"
34
+ hdf5_fmt: str = "iter{iter.total_iter}.h5"
35
+
36
+
37
+ class EnginePlan(Dataclass, kw_only=True):
38
+ sim_shape: t.Optional[t.Tuple[int, int]] = None
39
+ resize_method: t.Literal['pad_crop', 'resample'] = 'pad_crop'
40
+
41
+ probe_modes: int = 1
42
+ base_mode_power: float = 0.7
43
+ """Intensity to assign to the base mode when creating incoherent probe modes."""
44
+
45
+ bwlim_frac: t.Optional[float] = 2/3
46
+ obj_pad_px: float = 5.0
47
+
48
+ slices: t.Optional[Slices] = None
49
+
50
+ niter: int = 10
51
+ grouping: t.Optional[int] = None
52
+ compact: bool = False
53
+ shuffle_groups: t.Optional[FlagLike] = None
54
+ buffer_n_groups: int = 2
55
+
56
+ update_probe: FlagLike = True
57
+ update_object: FlagLike = True
58
+ update_positions: FlagLike = False
59
+
60
+ calc_error: FlagLike = Flag(every=1)
61
+ calc_error_fraction: float = 0.1
62
+
63
+ save: FlagLike = False
64
+ save_images: FlagLike = False
65
+ save_options: SaveOptions = SaveOptions()
66
+
67
+ send_every_group: bool = False
68
+
69
+
70
+ class AmplitudeNoisePlan(Dataclass, kw_only=True):
71
+ gaussian_variance: float = 0.1
72
+ eps: float = 1.0e-3
73
+ offset: float = 0.0
74
+
75
+
76
+ class AnscombeNoisePlan(AmplitudeNoisePlan, kw_only=True):
77
+ offset: float = 0.375
78
+
79
+
80
+ class PoissonNoisePlan(AmplitudeNoisePlan, kw_only=True):
81
+ eps: float = 1.0e-3
82
+
83
+
84
+ NoiseModelHook.known['amplitude'] = ('phaser.engines.common.noise_models:AmplitudeNoiseModel', AmplitudeNoisePlan)
85
+ NoiseModelHook.known['anscombe'] = ('phaser.engines.common.noise_models:AnscombeNoiseModel', AnscombeNoisePlan)
86
+ NoiseModelHook.known['poisson'] = ('phaser.engines.common.noise_models:PoissonNoiseModel', PoissonNoisePlan)
87
+
88
+
89
+ class LSQMLSolverPlan(Dataclass, kw_only=True):
90
+ stochastic: bool = True
91
+
92
+ beta_object: ScheduleLike = 1.0
93
+ beta_probe: ScheduleLike = 1.0
94
+
95
+ illum_reg_object: ScheduleLike = 1e-2
96
+ illum_reg_probe: ScheduleLike = 1e-2
97
+
98
+ gamma: ScheduleLike = 1e-4
99
+
100
+
101
+ class EPIESolverPlan(Dataclass, kw_only=True):
102
+ beta_object: ScheduleLike = 1.0
103
+ beta_probe: ScheduleLike = 1.0
104
+
105
+
106
+ ConventionalSolverHook.known['lsqml'] = ('phaser.engines.conventional.solvers:LSQMLSolver', LSQMLSolverPlan)
107
+ ConventionalSolverHook.known['epie'] = ('phaser.engines.conventional.solvers:EPIESolver', EPIESolverPlan)
108
+
109
+
110
+ class ConventionalEnginePlan(EnginePlan, kw_only=True):
111
+ noise_model: NoiseModelHook
112
+ solver: ConventionalSolverHook
113
+ position_solver: t.Optional[PositionSolverHook] = None
114
+
115
+ group_constraints: t.List[GroupConstraintHook]
116
+ iter_constraints: t.List[IterConstraintHook]
117
+
118
+
119
+ class GradientEnginePlan(EnginePlan):
120
+ noise_model: NoiseModelHook
121
+ solvers: t.Dict[ReconsVars, GradientSolverHook]
122
+
123
+ regularizers: t.List[CostRegularizerHook]
124
+ group_constraints: t.List[GroupConstraintHook]
125
+ iter_constraints: t.List[IterConstraintHook]
126
+
127
+
128
+ class SGDSolverPlan(Dataclass, kw_only=True):
129
+ learning_rate: ScheduleLike
130
+ momentum: t.Optional[ScheduleLike] = None
131
+ nesterov: bool = True
132
+
133
+
134
+ class AdamSolverPlan(Dataclass, kw_only=True):
135
+ learning_rate: ScheduleLike
136
+
137
+ b1: float = 0.9
138
+ b2: float = 0.999
139
+ eps: float = 1.0e-8
140
+ eps_root: float = 0.0
141
+
142
+ nesterov: bool = False
143
+
144
+
145
+ class PolyakSGDSolverPlan(Dataclass, kw_only=True):
146
+ max_learning_rate: ScheduleLike
147
+ f_min: float
148
+ scaling: ScheduleLike = 1.0
149
+ eps: float = 0.0
150
+
151
+
152
+ GradientSolverHook.known['sgd'] = ('phaser.engines.gradient.solvers:SGDSolver', SGDSolverPlan)
153
+ GradientSolverHook.known['adam'] = ('phaser.engines.gradient.solvers:AdamSolver', AdamSolverPlan)
154
+ GradientSolverHook.known['polyak_sgd'] = ('phaser.engines.gradient.solvers:PolyakSGDSolver', PolyakSGDSolverPlan)
155
+
156
+ EngineHook.known['conventional'] = ('phaser.engines.conventional.run:run_engine', ConventionalEnginePlan)
157
+ EngineHook.known['gradient'] = ('phaser.engines.gradient.run:run_engine', GradientEnginePlan)
158
+
159
+
160
+ class ReconsPlan(Dataclass, kw_only=True):
161
+ file_type: t.Literal['phaser_plan'] = 'phaser_plan'
162
+ version: t.Annotated[str, IsVersion(exactly="1.0")] = "1.0"
163
+
164
+ name: str
165
+
166
+ backend: t.Optional[BackendName] = None
167
+ dtype: t.Literal['float32', 'float64'] = 'float32'
168
+
169
+ wavelength: t.Optional[float] = None
170
+
171
+ raw_data: RawDataHook
172
+
173
+ post_load: t.Sequence[PostLoadHook] = ()
174
+
175
+ init: InitPlan = InitPlan()
176
+
177
+ post_init: t.Sequence[PostInitHook] = ()
178
+
179
+ slices: t.Optional[Slices] = None
180
+
181
+ engines: t.List[EngineHook]
182
+ #engines: t.List[t.Annotated[t.Union[ConventionalEngine, GradientEngine], Tagged('type')]]
183
+
184
+
phaser/py.typed ADDED
File without changes