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
@@ -0,0 +1,88 @@
1
+
2
+ from pathlib import Path
3
+ import warnings
4
+ import logging
5
+ import typing as t
6
+
7
+ import numpy
8
+
9
+ from phaser.utils.num import Sampling
10
+ from phaser.utils.physics import Electron
11
+ from phaser.io.empad import load_4d, EmpadMetadata
12
+ from phaser.types import cast_length
13
+ from .. import LoadEmpadProps, RawData
14
+
15
+
16
+ def load_empad(args: None, props: LoadEmpadProps) -> RawData:
17
+ logger = logging.getLogger(__name__)
18
+
19
+ path = Path(props.path).expanduser()
20
+
21
+ if path.suffix.lower() == '.json': # load as metadata
22
+ meta = EmpadMetadata.from_json(path)
23
+ assert meta.path is not None
24
+
25
+ path = meta.path / meta.raw_filename
26
+
27
+ voltage = props.kv * 1e3 if props.kv is not None else meta.voltage
28
+ diff_step = props.diff_step or meta.diff_step
29
+ scan_shape = meta.scan_shape
30
+ adu = props.adu or meta.adu
31
+ needs_scale = not meta.is_simulated()
32
+
33
+ probe_hook = {
34
+ 'type': 'focused',
35
+ 'conv_angle': meta.conv_angle,
36
+ 'defocus': meta.defocus * 1e10 if meta.defocus is not None else None,
37
+ }
38
+ # TODO: handle explicit scan_positions here
39
+ scan_hook = {
40
+ 'type': 'raster',
41
+ # [x, y] -> [y, x]
42
+ 'shape': tuple(reversed(meta.scan_shape)),
43
+ 'step_size': tuple(s*1e10 for s in reversed(meta.scan_step)), # m to A
44
+ 'affine': meta.scan_correction[::-1, ::-1] if meta.scan_correction is not None else None,
45
+ }
46
+
47
+ else:
48
+ voltage = props.kv * 1e3 if props.kv is not None else None
49
+ diff_step = props.diff_step
50
+ scan_shape = None
51
+ probe_hook = scan_hook = None
52
+ adu = None
53
+ needs_scale = False
54
+
55
+ if voltage is None:
56
+ raise ValueError("'kv'/'voltage' must be specified by metadata or passed to 'raw_data'")
57
+ if diff_step is None:
58
+ raise ValueError("'diff_step' must be specified by metadata or passed to 'raw_data'")
59
+
60
+ wavelength = Electron(voltage).wavelength
61
+
62
+ if not path.exists():
63
+ raise ValueError(f"Couldn't find raw data at path {path}")
64
+
65
+ patterns = numpy.fft.ifftshift(load_4d(path, scan_shape, memmap=True), axes=(-1, -2))
66
+
67
+ if needs_scale:
68
+ if adu is None:
69
+ warnings.warn("ADU not supplied for experimental dataset. This is not recommended.")
70
+ else:
71
+ logger.info(f"Scaling patterns by ADU ({adu:.1f})")
72
+ patterns /= adu
73
+
74
+ a = wavelength / (diff_step * 1e-3) # recip. pixel size -> 1 / real space extent
75
+ sampling = Sampling(cast_length(patterns.shape[-2:], 2), extent=(a, a))
76
+
77
+ mask = numpy.zeros_like(patterns, shape=patterns.shape[-2:])
78
+ mask[2:-2, 2:-2] = 1.
79
+
80
+ return {
81
+ 'patterns': patterns,
82
+ 'mask': numpy.fft.ifftshift(mask, axes=(-1, -2)),
83
+ 'sampling': sampling,
84
+ 'wavelength': wavelength,
85
+ 'probe_hook': probe_hook,
86
+ 'scan_hook': scan_hook,
87
+ 'seed': None,
88
+ }
phaser/hooks/object.py ADDED
@@ -0,0 +1,25 @@
1
+
2
+ import numpy
3
+
4
+ from phaser.utils.num import cast_array_module, to_complex_dtype
5
+ from phaser.utils.object import random_phase_object
6
+ from ..state import ObjectState
7
+ from . import ObjectHookArgs, RandomObjectProps
8
+
9
+
10
+ def random_object(args: ObjectHookArgs, props: RandomObjectProps) -> ObjectState:
11
+ sampling = args['sampling']
12
+
13
+ if args['slices'] is not None:
14
+ thicknesses = numpy.array(args['slices'].thicknesses, dtype=args['dtype'])
15
+ shape = (len(thicknesses), *sampling.shape)
16
+ else:
17
+ thicknesses = numpy.array([], dtype=args['dtype'])
18
+ shape = sampling.shape
19
+
20
+ obj = random_phase_object(
21
+ shape, props.sigma,
22
+ dtype=to_complex_dtype(args['dtype']),
23
+ xp=cast_array_module(args['xp'])
24
+ )
25
+ return ObjectState(sampling, obj, thicknesses)
@@ -0,0 +1,133 @@
1
+ import logging
2
+ import math
3
+ import typing as t
4
+
5
+ import numpy
6
+ from numpy.typing import NDArray
7
+
8
+ from phaser.types import cast_length
9
+ from phaser.utils.num import get_array_module, cast_array_module, to_numpy, fft2, ifft2, at, Sampling
10
+ from phaser.utils.misc import create_rng, create_sparse_groupings
11
+ from phaser.utils.optics import fourier_shift_filter
12
+ from phaser.utils.image import affine_transform
13
+ from phaser.state import Patterns, ReconsState
14
+ from . import RawData, PostInitArgs, PoissonProps, ScaleProps, DropNanProps, CropDataProps
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def crop_data(raw_data: RawData, props: CropDataProps) -> RawData:
20
+ if raw_data['patterns'].ndim != 4:
21
+ raise ValueError(f"'crop_data' expects a 4D array of patterns, got shape {raw_data['patterns'].shape} instead")
22
+
23
+ (y_i, y_f, x_i, x_f) = props.crop
24
+ logging.info(f"Cropping raw data to {0 if y_i is None else y_i}:{raw_data['patterns'].shape[0] if y_f is None else y_f},"
25
+ f" {0 if x_i is None else x_i}:{raw_data['patterns'].shape[1] if x_f is None else x_f}")
26
+ raw_data['patterns'] = raw_data['patterns'][slice(y_i, y_f), slice(x_i, x_f)]
27
+
28
+ if raw_data['scan_hook'] is not None:
29
+ if raw_data['scan_hook']['type'] == 'raster':
30
+ raw_data['scan_hook'] = {
31
+ **raw_data['scan_hook'],
32
+ 'shape': raw_data['patterns'].shape[:2],
33
+ }
34
+
35
+ return raw_data
36
+
37
+
38
+ def scale_patterns(raw_data: RawData, props: ScaleProps) -> RawData:
39
+ raw_data['patterns'] *= props.scale
40
+ return raw_data
41
+
42
+
43
+ def add_poisson_noise(raw_data: RawData, props: PoissonProps) -> RawData:
44
+ xp = get_array_module(raw_data['patterns'])
45
+ dtype = raw_data['patterns'].dtype
46
+
47
+ if props.scale is not None:
48
+ logger.info(f"Adding poisson noise to raw patterns, after scaling by {props.scale:.2e}")
49
+ raw_data['patterns'] *= props.scale
50
+ else:
51
+ logger.info("Adding poisson noise to raw patterns")
52
+
53
+ rng = create_rng(raw_data['seed'], 'poisson_noise')
54
+
55
+ # TODO do this in batches?
56
+ patterns = rng.poisson(to_numpy(raw_data['patterns'])).astype(dtype)
57
+
58
+ if props.gaussian is not None:
59
+ patterns += rng.normal(scale=props.gaussian, size=patterns.shape)
60
+
61
+ logger.info(f"Mean pattern intensity: {numpy.nanmean(numpy.nansum(patterns, axis=(-1, -2)))}")
62
+
63
+ raw_data['patterns'] = xp.array(patterns)
64
+ return raw_data
65
+
66
+
67
+ def drop_nan_patterns(args: PostInitArgs, props: DropNanProps) -> t.Tuple[Patterns, ReconsState]:
68
+ xp = get_array_module(args['data'].patterns)
69
+
70
+ # flatten scan and patterns
71
+ scan = args['state'].scan.reshape(-1, 2)
72
+ patterns = args['data'].patterns.reshape(-1, *args['data'].patterns.shape[-2:])
73
+
74
+ fraction_nan = xp.sum(xp.isnan(args['data'].patterns), axis=(-1, -2)) / xp.prod(patterns.shape[-2:])
75
+
76
+ mask = fraction_nan > props.threshold
77
+
78
+ if (n := int(xp.sum(mask))):
79
+ logger.info(f"Dropping {n}/{patterns.shape[0]} patterns which are at least {props.threshold:.1%} NaN values")
80
+ patterns = patterns[mask]
81
+
82
+ if scan.shape[0] == mask.size:
83
+ # apply mask to scan as well
84
+ scan = scan[mask]
85
+ elif scan.shape[0] != patterns.shape[0]:
86
+ raise ValueError(f"# of scan positions {scan.shape[0]} doesn't match # of patterns"
87
+ f" before ({mask.size}) or after ({patterns.shape[0]}) filtering")
88
+ # otherwise, we assume the mask has already been applied to the scan
89
+
90
+ args['state'].scan = scan
91
+ args['data'].patterns = patterns
92
+
93
+ return (args['data'], args['state'])
94
+
95
+
96
+ def diffraction_align(args: PostInitArgs, props: t.Any = None) -> t.Tuple[Patterns, ReconsState]:
97
+ patterns, state = args['data'], args['state']
98
+
99
+ xp = cast_array_module(args['xp'])
100
+ grouping = 128
101
+ groups = create_sparse_groupings(patterns.patterns.shape[:-2], grouping)
102
+
103
+ sum_pattern = xp.zeros(patterns.patterns.shape[-2:], dtype=patterns.patterns.dtype)
104
+
105
+ for group in groups:
106
+ pats = xp.array(patterns.patterns[tuple(group)]) * xp.array(patterns.pattern_mask)
107
+ sum_pattern += t.cast(NDArray[numpy.floating], xp.nansum(pats, axis=tuple(range(pats.ndim - 2))))
108
+
109
+ mean_pattern = sum_pattern / math.prod(patterns.patterns.shape[:-2])
110
+
111
+ ky, kx = Sampling(
112
+ cast_length(mean_pattern.shape, 2), extent=(1.0, 1.0)
113
+ ).recip_grid(dtype=patterns.patterns.dtype, xp=xp)
114
+
115
+ shift = xp.array([
116
+ xp.nansum(ky * mean_pattern), xp.nansum(kx * mean_pattern)
117
+ ]) / xp.nansum(mean_pattern)
118
+
119
+ logging.info(f"Shifting diffraction patterns by ({shift[1]}, {shift[0]}) px")
120
+
121
+ def bilinear_shift(arr: NDArray[numpy.floating]) -> NDArray[numpy.floating]:
122
+ return to_numpy(xp.fft.ifftshift(affine_transform(
123
+ xp.fft.fftshift(xp.array(arr), axes=(-2, -1)), [1., 1.], shift,
124
+ output_shape=arr.shape[-2:], order=1
125
+ ), axes=(-2, -1)))
126
+
127
+ for group in groups:
128
+ patterns.patterns[tuple(group)] = bilinear_shift(patterns.patterns[tuple(group)])
129
+
130
+ # fftshift mask as well
131
+ patterns.pattern_mask = bilinear_shift(patterns.pattern_mask)
132
+
133
+ return (patterns, state)
phaser/hooks/probe.py ADDED
@@ -0,0 +1,24 @@
1
+ import logging
2
+
3
+ from phaser.utils.optics import make_focused_probe
4
+ from ..state import ProbeState
5
+ from . import ProbeHookArgs, FocusedProbeProps
6
+
7
+
8
+ def focused_probe(args: ProbeHookArgs, props: FocusedProbeProps) -> ProbeState:
9
+ logger = logging.getLogger(__name__)
10
+
11
+ if props.conv_angle is None:
12
+ raise ValueError("Probe 'conv_angle' must be specified by metadata or manually")
13
+ if props.defocus is None:
14
+ raise ValueError("Probe 'defocus' must be specified by metadata or manually")
15
+
16
+ logger.info(f"Making probe, conv_angle {props.conv_angle} mrad, defocus {props.defocus} A")
17
+
18
+ sampling = args['sampling']
19
+ ky, kx = sampling.recip_grid(dtype=args['dtype'], xp=args['xp'])
20
+ probe = make_focused_probe(
21
+ ky, kx, args['wavelength'],
22
+ props.conv_angle, defocus=props.defocus
23
+ )
24
+ return ProbeState(sampling, probe)
@@ -0,0 +1,97 @@
1
+ import abc
2
+ import typing as t
3
+
4
+ import numpy
5
+ from numpy.typing import NDArray
6
+
7
+ from phaser.types import Dataclass
8
+ from phaser.utils.num import Float
9
+ from . import Hook
10
+
11
+ from .solver import HasState, StateT
12
+
13
+ if t.TYPE_CHECKING:
14
+ from phaser.state import ReconsState
15
+
16
+
17
+ @t.runtime_checkable
18
+ class GroupConstraint(HasState[StateT], t.Protocol[StateT]):
19
+ def apply_group(self, group: NDArray[numpy.integer], sim: 'ReconsState', state: StateT) -> t.Tuple['ReconsState', StateT]:
20
+ ...
21
+
22
+
23
+ @t.runtime_checkable
24
+ class IterConstraint(HasState[StateT], t.Protocol[StateT]):
25
+ def apply_iter(self, sim: 'ReconsState', state: StateT) -> t.Tuple['ReconsState', StateT]:
26
+ ...
27
+
28
+
29
+ @t.runtime_checkable
30
+ class CostRegularizer(HasState[StateT], t.Protocol[StateT]):
31
+ def calc_loss_group(self, group: NDArray[numpy.integer], sim: 'ReconsState', state: StateT) -> t.Tuple[Float, StateT]:
32
+ ...
33
+
34
+
35
+ class ClampObjectAmplitudeProps(Dataclass):
36
+ amplitude: float = 1.1
37
+
38
+
39
+ class LimitProbeSupportProps(Dataclass):
40
+ max_angle: float
41
+
42
+
43
+ class RegularizeLayersProps(Dataclass):
44
+ weight: float = 0.9 # weight of regularization to apply
45
+ sigma: float = 50.0 # standard deviation of gaussian filter
46
+
47
+
48
+ class ObjLowPassProps(Dataclass):
49
+ max_freq: float = 0.4 # 1/px (nyquist = 0.5)
50
+
51
+
52
+ class IterConstraintHook(Hook[None, IterConstraint]):
53
+ known = {
54
+ 'clamp_object_amplitude': ('phaser.engines.common.regularizers:ClampObjectAmplitude', ClampObjectAmplitudeProps),
55
+ 'limit_probe_support': ('phaser.engines.common.regularizers:LimitProbeSupport', LimitProbeSupportProps),
56
+ 'layers': ('phaser.engines.common.regularizers:RegularizeLayers', RegularizeLayersProps),
57
+ 'obj_low_pass': ('phaser.engines.common.regularizers:ObjLowPass', ObjLowPassProps),
58
+ 'remove_phase_ramp': ('phaser.engines.common.regularizers:RemovePhaseRamp', t.Dict[str, t.Any]),
59
+ }
60
+
61
+
62
+ class GroupConstraintHook(Hook[None, GroupConstraint]):
63
+ known = {
64
+ 'clamp_object_amplitude': ('phaser.engines.common.regularizers:ClampObjectAmplitude', ClampObjectAmplitudeProps),
65
+ 'limit_probe_support': ('phaser.engines.common.regularizers:LimitProbeSupport', LimitProbeSupportProps),
66
+ 'obj_low_pass': ('phaser.engines.common.regularizers:ObjLowPass', ObjLowPassProps),
67
+ 'remove_phase_ramp': ('phaser.engines.common.regularizers:RemovePhaseRamp', t.Dict[str, t.Any]),
68
+ }
69
+
70
+
71
+ class CostRegularizerProps(Dataclass):
72
+ cost: float
73
+
74
+
75
+ class TVRegularizerProps(Dataclass):
76
+ cost: float
77
+ eps: float = 1.0e-8
78
+
79
+
80
+ class CostRegularizerHook(Hook[None, CostRegularizer]):
81
+ known = {
82
+ 'obj_l1': ('phaser.engines.common.regularizers:ObjL1', CostRegularizerProps),
83
+ 'obj_l2': ('phaser.engines.common.regularizers:ObjL2', CostRegularizerProps),
84
+ 'obj_phase_l1': ('phaser.engines.common.regularizers:ObjPhaseL1', CostRegularizerProps),
85
+ 'obj_recip_l1': ('phaser.engines.common.regularizers:ObjRecipL1', CostRegularizerProps),
86
+ 'obj_tv': ('phaser.engines.common.regularizers:ObjTotalVariation', TVRegularizerProps),
87
+ 'obj_tikh': ('phaser.engines.common.regularizers:ObjTikhonov', CostRegularizerProps),
88
+ 'obj_tikhonov': ('phaser.engines.common.regularizers:ObjTikhonov', CostRegularizerProps),
89
+ 'layers_tv': ('phaser.engines.common.regularizers:LayersTotalVariation', CostRegularizerProps),
90
+ 'layers_tikh': ('phaser.engines.common.regularizers:LayersTikhonov', CostRegularizerProps),
91
+ 'layers_tikhonov': ('phaser.engines.common.regularizers:LayersTikhonov', CostRegularizerProps),
92
+ 'probe_phase_tikh': ('phaser.engines.common.regularizers:ProbePhaseTikhonov', CostRegularizerProps),
93
+ 'probe_phase_tikhonov': ('phaser.engines.common.regularizers:ProbePhaseTikhonov', CostRegularizerProps),
94
+ 'probe_recip_tv': ('phaser.engines.common.regularizers:ProbeRecipTotalVariation', TVRegularizerProps),
95
+ 'probe_recip_tikh': ('phaser.engines.common.regularizers:ProbeRecipTikhonov', CostRegularizerProps),
96
+ 'probe_recip_tikhonov': ('phaser.engines.common.regularizers:ProbeRecipTikhonov', CostRegularizerProps),
97
+ }
phaser/hooks/scan.py ADDED
@@ -0,0 +1,27 @@
1
+
2
+ import numpy
3
+ from numpy.typing import NDArray
4
+
5
+ from phaser.utils.num import cast_array_module
6
+ from phaser.utils.scan import make_raster_scan
7
+ from . import ScanHookArgs, RasterScanProps
8
+
9
+
10
+ def raster_scan(args: ScanHookArgs, props: RasterScanProps) -> NDArray[numpy.floating]:
11
+ xp = cast_array_module(args['xp'])
12
+
13
+ if props.shape is None:
14
+ raise ValueError("scan 'shape' must be specified by metadata or manually")
15
+ if props.step_size is None:
16
+ raise ValueError("scan 'step_size' must be specified by metadata or manually")
17
+
18
+ scan = make_raster_scan(
19
+ props.shape, props.step_size, props.rotation or 0.0,
20
+ dtype=args['dtype'], xp=xp,
21
+ )
22
+
23
+ if props.affine is not None:
24
+ affine = xp.array(props.affine, dtype=scan.dtype)
25
+ scan = scan @ affine
26
+
27
+ return scan
@@ -0,0 +1,75 @@
1
+ import logging
2
+ import typing as t
3
+
4
+ import numpy
5
+
6
+ from ..types import Dataclass, Flag, process_schedule
7
+ from .hook import Hook
8
+
9
+ if t.TYPE_CHECKING:
10
+ from ..state import ReconsState
11
+
12
+
13
+ class FlagArgs(t.TypedDict):
14
+ state: 'ReconsState'
15
+ niter: int
16
+
17
+
18
+ class FlagHook(Hook[FlagArgs, bool]):
19
+ known = {}
20
+
21
+
22
+ class ScheduleHook(Hook[FlagArgs, float]):
23
+ known = {}
24
+
25
+
26
+ FlagLike: t.TypeAlias = t.Union[bool, Flag, FlagHook]
27
+ ScheduleLike: t.TypeAlias = t.Union[float, ScheduleHook]
28
+
29
+
30
+ # TODO: make these hooks two-step
31
+
32
+
33
+ class ConstantScheduleProps(Dataclass):
34
+ value: float
35
+
36
+
37
+ def constant_schedule(args: FlagArgs, props: ConstantScheduleProps) -> float:
38
+ return props.value
39
+
40
+
41
+ class PiecewiseScheduleProps(Dataclass):
42
+ init: ScheduleLike
43
+ steps: t.Dict[int, ScheduleLike]
44
+
45
+
46
+ def piecewise_schedule(args: FlagArgs, props: PiecewiseScheduleProps) -> float:
47
+ i = args['state'].iter.engine_iter
48
+
49
+ for thresh, val in sorted(props.steps.items(), key=lambda t: t[0], reverse=True):
50
+ if i >= thresh:
51
+ return process_schedule(val)(args)
52
+
53
+ return process_schedule(props.init)(args)
54
+
55
+
56
+ class ExprScheduleProps(Dataclass):
57
+ expr: str
58
+
59
+
60
+ def expr_schedule(args: FlagArgs, props: ExprScheduleProps) -> float:
61
+ val = float(eval(props.expr, {
62
+ 'i': args['state'].iter.engine_iter,
63
+ 'iter': args['state'].iter,
64
+ 'state': args['state'],
65
+ 'niter': args['niter'],
66
+ 'np': numpy,
67
+ }))
68
+ logging.getLogger(__name__).debug(f"expr_schedule expr: {props.expr} value: {val}")
69
+
70
+ return val
71
+
72
+
73
+ ScheduleHook.known['constant'] = ('phaser.hooks.schedule:constant_schedule', ConstantScheduleProps)
74
+ ScheduleHook.known['piecewise'] = ('phaser.hooks.schedule:piecewise_schedule', PiecewiseScheduleProps)
75
+ ScheduleHook.known['expr'] = ('phaser.hooks.schedule:expr_schedule', ExprScheduleProps)
phaser/hooks/solver.py ADDED
@@ -0,0 +1,169 @@
1
+ import abc
2
+ import typing as t
3
+
4
+ import numpy
5
+ from numpy.typing import NDArray
6
+
7
+ from phaser.types import Dataclass, ReconsVar
8
+ from phaser.utils.num import Float
9
+ from . import Hook
10
+
11
+ StateT = t.TypeVar('StateT')
12
+
13
+ if t.TYPE_CHECKING:
14
+ from phaser.engines.common.simulation import SimulationState
15
+ from phaser.execute import Observer
16
+ from phaser.plan import ConventionalEnginePlan, GradientEnginePlan
17
+ from phaser.state import ReconsState
18
+
19
+
20
+ class HasState(t.Protocol[StateT]): # type: ignore
21
+ def init_state(self, sim: 'ReconsState') -> StateT:
22
+ ...
23
+
24
+
25
+ class NoiseModel(HasState[StateT], t.Protocol[StateT]):
26
+ @classmethod
27
+ def name(cls) -> str:
28
+ ...
29
+
30
+ def calc_loss(
31
+ self,
32
+ model_wave: NDArray[numpy.complexfloating],
33
+ model_intensity: NDArray[numpy.floating],
34
+ exp_patterns: NDArray[numpy.floating],
35
+ mask: NDArray[numpy.floating],
36
+ state: StateT,
37
+ ) -> t.Tuple[Float, StateT]:
38
+ """
39
+ Return the calculated loss, summed across the detector and averaged across the scan.
40
+
41
+ May be called in a JAX jit context, so must have no side effects.
42
+ """
43
+ ...
44
+
45
+ def calc_wave_update(
46
+ self,
47
+ model_wave: NDArray[numpy.complexfloating],
48
+ model_intensity: NDArray[numpy.floating],
49
+ exp_patterns: NDArray[numpy.floating],
50
+ mask: NDArray[numpy.floating],
51
+ state: StateT,
52
+ ) -> t.Tuple[NDArray[numpy.complexfloating], StateT]:
53
+ """
54
+ Return the calculated wave update `chi` in reciprocal space.
55
+
56
+ May be called in a JAX jit context, so must have no side effects.
57
+ """
58
+ ...
59
+
60
+
61
+ class NoiseModelHook(Hook[None, NoiseModel]):
62
+ known = {}
63
+
64
+
65
+ class PositionSolver(HasState[StateT], t.Protocol[StateT]):
66
+ def perform_update(
67
+ self,
68
+ positions: NDArray[numpy.floating],
69
+ gradients: NDArray[numpy.floating],
70
+ state: StateT
71
+ ) -> t.Tuple[NDArray[numpy.floating], StateT]:
72
+ """
73
+ Return the calculated position updates
74
+ """
75
+ ...
76
+
77
+
78
+ class SteepestDescentPositionSolverProps(Dataclass):
79
+ # fraction of optimal step to take
80
+ step_size: float = 1e-2
81
+ # maximum step size (in angstroms)
82
+ max_step_size: t.Optional[float] = None
83
+
84
+
85
+ class MomentumPositionSolverProps(Dataclass):
86
+ # fraction of optimal step to take
87
+ step_size: float = 1e-2
88
+ # maximum step size (in angstroms)
89
+ max_step_size: t.Optional[float] = None
90
+ # momentum decay rate
91
+ momentum: float = 0.9
92
+
93
+
94
+ class PositionSolverHook(Hook[None, PositionSolver]):
95
+ known = {
96
+ 'steepest_descent': ('phaser.engines.common.position_correction:SteepestDescentPositionSolver', SteepestDescentPositionSolverProps),
97
+ 'momentum': ('phaser.engines.common.position_correction:MomentumPositionSolver', MomentumPositionSolverProps),
98
+ }
99
+
100
+
101
+ class ConventionalSolver(abc.ABC):
102
+ @classmethod
103
+ @abc.abstractmethod
104
+ def name(cls) -> str:
105
+ ...
106
+
107
+ @abc.abstractmethod
108
+ def init(self, sim: 'SimulationState') -> 'SimulationState':
109
+ ...
110
+
111
+ @abc.abstractmethod
112
+ def presolve(
113
+ self,
114
+ sim: 'SimulationState',
115
+ groups: t.Iterator[NDArray[numpy.int_]], *,
116
+ patterns: NDArray[numpy.floating],
117
+ pattern_mask: NDArray[numpy.floating],
118
+ propagators: t.Optional[NDArray[numpy.complexfloating]],
119
+ ) -> 'SimulationState':
120
+ ...
121
+
122
+ @abc.abstractmethod
123
+ def run_iteration(
124
+ self,
125
+ sim: 'SimulationState',
126
+ groups: t.Iterator[NDArray[numpy.int_]], *,
127
+ patterns: NDArray[numpy.floating],
128
+ pattern_mask: NDArray[numpy.floating],
129
+ propagators: t.Optional[NDArray[numpy.complexfloating]],
130
+ update_object: bool,
131
+ update_probe: bool,
132
+ update_positions: bool,
133
+ calc_error: bool,
134
+ calc_error_mask: NDArray[numpy.bool_],
135
+ observer: 'Observer',
136
+ ) -> t.Tuple['SimulationState', NDArray[numpy.floating], t.List[NDArray[numpy.floating]]]:
137
+ """
138
+ Run an iteration of the reconstruction. Return a tuple `(sim, pos_update, iter_errors)`.
139
+ """
140
+ ...
141
+
142
+
143
+ class ConventionalSolverHook(Hook['ConventionalEnginePlan', ConventionalSolver]):
144
+ known = {}
145
+
146
+
147
+ class GradientSolver(HasState[StateT], t.Protocol[StateT]):
148
+ name: str
149
+ params: t.FrozenSet[ReconsVar]
150
+
151
+ def init_state(self, sim: 'ReconsState') -> StateT:
152
+ ...
153
+
154
+ def update_for_iter(self, sim: 'ReconsState', state: StateT, niter: int) -> StateT:
155
+ ...
156
+
157
+ def update(
158
+ self, sim: 'ReconsState', state: StateT, grad: t.Dict[ReconsVar, numpy.ndarray], loss: float,
159
+ ) -> t.Tuple[t.Dict[ReconsVar, numpy.ndarray], StateT]:
160
+ ...
161
+
162
+
163
+ class GradientSolverArgs(t.TypedDict):
164
+ plan: 'GradientEnginePlan'
165
+ params: t.Iterable[ReconsVar]
166
+
167
+
168
+ class GradientSolverHook(Hook['GradientSolverArgs', GradientSolver]):
169
+ known = {}
phaser/io/__init__.py ADDED
File without changes