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.
- phaser/__init__.py +0 -0
- phaser/__main__.py +5 -0
- phaser/engines/common/__init__.py +0 -0
- phaser/engines/common/noise_models.py +113 -0
- phaser/engines/common/output.py +189 -0
- phaser/engines/common/position_correction.py +62 -0
- phaser/engines/common/regularizers.py +403 -0
- phaser/engines/common/simulation.py +270 -0
- phaser/engines/conventional/__init__.py +0 -0
- phaser/engines/conventional/run.py +142 -0
- phaser/engines/conventional/solvers.py +476 -0
- phaser/engines/gradient/run.py +451 -0
- phaser/engines/gradient/solvers.py +139 -0
- phaser/execute.py +371 -0
- phaser/hooks/__init__.py +158 -0
- phaser/hooks/hook.py +159 -0
- phaser/hooks/io/empad.py +88 -0
- phaser/hooks/object.py +25 -0
- phaser/hooks/preprocessing.py +133 -0
- phaser/hooks/probe.py +24 -0
- phaser/hooks/regularization.py +97 -0
- phaser/hooks/scan.py +27 -0
- phaser/hooks/schedule.py +75 -0
- phaser/hooks/solver.py +169 -0
- phaser/io/__init__.py +0 -0
- phaser/io/empad.py +212 -0
- phaser/main.py +92 -0
- phaser/plan.py +184 -0
- phaser/py.typed +0 -0
- phaser/state.py +249 -0
- phaser/types.py +305 -0
- phaser/utils/__init__.py +0 -0
- phaser/utils/_cuda_kernels.py +213 -0
- phaser/utils/_jax_kernels.py +98 -0
- phaser/utils/analysis.py +263 -0
- phaser/utils/image.py +201 -0
- phaser/utils/io.py +402 -0
- phaser/utils/misc.py +295 -0
- phaser/utils/num.py +800 -0
- phaser/utils/object.py +578 -0
- phaser/utils/optics.py +377 -0
- phaser/utils/physics.py +88 -0
- phaser/utils/plotting.py +699 -0
- phaser/utils/scan.py +60 -0
- phaser/web/__init__.py +0 -0
- phaser/web/dist/03510a839ccb97b0da9f.module.wasm +0 -0
- phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
- phaser/web/dist/bundle-dashboard.js +712 -0
- phaser/web/dist/bundle-manager.js +210 -0
- phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +106 -0
- phaser/web/dist/style.css +152 -0
- phaser/web/notebook.py +269 -0
- phaser/web/routes.py +211 -0
- phaser/web/server.py +540 -0
- phaser/web/slurm.py +180 -0
- phaser/web/templates/base.html +13 -0
- phaser/web/templates/dashboard.html +14 -0
- phaser/web/templates/manager.html +19 -0
- phaser/web/types.py +255 -0
- phaser/web/util.py +116 -0
- phaser/web/worker.py +200 -0
- phaserem-0.1.dist-info/METADATA +121 -0
- phaserem-0.1.dist-info/RECORD +67 -0
- phaserem-0.1.dist-info/WHEEL +5 -0
- phaserem-0.1.dist-info/entry_points.txt +2 -0
- phaserem-0.1.dist-info/licenses/LICENSE.txt +373 -0
- phaserem-0.1.dist-info/top_level.txt +1 -0
phaser/hooks/io/empad.py
ADDED
|
@@ -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
|
phaser/hooks/schedule.py
ADDED
|
@@ -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
|