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/__init__.py
ADDED
|
File without changes
|
phaser/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
from phaser.hooks.solver import NoiseModel
|
|
8
|
+
from phaser.plan import AmplitudeNoisePlan, AnscombeNoisePlan, PoissonNoisePlan
|
|
9
|
+
from phaser.utils.num import get_array_module, Float
|
|
10
|
+
from phaser.state import ReconsState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AmplitudeNoiseModel(NoiseModel[None]):
|
|
14
|
+
@classmethod
|
|
15
|
+
def name(cls) -> str:
|
|
16
|
+
return "amplitude"
|
|
17
|
+
|
|
18
|
+
def __init__(self, args: None, props: AmplitudeNoisePlan):
|
|
19
|
+
self.offset: float = props.offset
|
|
20
|
+
self.gaussian_variance: float = props.gaussian_variance
|
|
21
|
+
|
|
22
|
+
self.var: float = 1 + self.gaussian_variance
|
|
23
|
+
|
|
24
|
+
self.eps: float = props.eps
|
|
25
|
+
|
|
26
|
+
def init_state(self, sim: ReconsState) -> None:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def calc_loss(
|
|
30
|
+
self,
|
|
31
|
+
model_wave: NDArray[numpy.complexfloating],
|
|
32
|
+
model_intensity: NDArray[numpy.floating],
|
|
33
|
+
exp_patterns: NDArray[numpy.floating],
|
|
34
|
+
mask: NDArray[numpy.floating],
|
|
35
|
+
state: None
|
|
36
|
+
) -> t.Tuple[Float, None]:
|
|
37
|
+
xp = get_array_module(model_wave, model_intensity, exp_patterns, mask)
|
|
38
|
+
patterns = xp.maximum(exp_patterns, 0.0)
|
|
39
|
+
|
|
40
|
+
return ((
|
|
41
|
+
2. * xp.sum(mask * (
|
|
42
|
+
xp.sqrt(patterns + self.offset) - xp.sqrt(model_intensity + self.offset) - self.eps
|
|
43
|
+
)**2) / self.var).astype(exp_patterns.dtype),
|
|
44
|
+
state
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def calc_wave_update(
|
|
48
|
+
self,
|
|
49
|
+
model_wave: NDArray[numpy.complexfloating],
|
|
50
|
+
model_intensity: NDArray[numpy.floating],
|
|
51
|
+
exp_patterns: NDArray[numpy.floating],
|
|
52
|
+
mask: NDArray[numpy.floating],
|
|
53
|
+
state: None
|
|
54
|
+
) -> t.Tuple[NDArray[numpy.complexfloating], None]:
|
|
55
|
+
xp = get_array_module(model_wave, model_intensity, exp_patterns, mask)
|
|
56
|
+
patterns = xp.maximum(exp_patterns, 0.0)
|
|
57
|
+
|
|
58
|
+
update = xp.sqrt(patterns + self.offset) / (xp.sqrt(model_intensity + self.offset) + self.eps) - 1.0
|
|
59
|
+
update *= mask # / self.var
|
|
60
|
+
#print(f"min update: {xp.min(update).get()}")
|
|
61
|
+
#print(f"max update: {xp.max(update).get()}")
|
|
62
|
+
|
|
63
|
+
return (update * model_wave, state)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AnscombeNoiseModel(AmplitudeNoiseModel):
|
|
67
|
+
@classmethod
|
|
68
|
+
def name(cls) -> str:
|
|
69
|
+
return "anscombe"
|
|
70
|
+
|
|
71
|
+
def __init__(self, args: None, props: AnscombeNoisePlan):
|
|
72
|
+
super().__init__(args, props)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PoissonNoiseModel(NoiseModel[None]):
|
|
76
|
+
@classmethod
|
|
77
|
+
def name(cls) -> str:
|
|
78
|
+
return "poisson"
|
|
79
|
+
|
|
80
|
+
def __init__(self, args: None, props: PoissonNoisePlan):
|
|
81
|
+
self.eps: float = props.eps
|
|
82
|
+
|
|
83
|
+
def init_state(self, sim: ReconsState) -> None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def calc_loss(
|
|
87
|
+
self,
|
|
88
|
+
model_wave: NDArray[numpy.complexfloating],
|
|
89
|
+
model_intensity: NDArray[numpy.floating],
|
|
90
|
+
exp_patterns: NDArray[numpy.floating],
|
|
91
|
+
mask: NDArray[numpy.floating],
|
|
92
|
+
state: None
|
|
93
|
+
) -> t.Tuple[Float, None]:
|
|
94
|
+
xp = get_array_module(model_wave, model_intensity, exp_patterns, mask)
|
|
95
|
+
patterns = xp.maximum(exp_patterns, 0.0)
|
|
96
|
+
#intensity - patterns * xp.log(intensity + self.offset)
|
|
97
|
+
|
|
98
|
+
loss = xp.sum(mask * (
|
|
99
|
+
#model_intensity - patterns * xp.log(model_intensity + self.eps)
|
|
100
|
+
model_intensity + patterns * (xp.log(patterns + self.eps) - xp.log(model_intensity + self.eps) - 1.0)
|
|
101
|
+
#patterns - (model_intensity + self.offset) * xp.log(patterns)
|
|
102
|
+
)).astype(exp_patterns.dtype)
|
|
103
|
+
return (loss, state)
|
|
104
|
+
|
|
105
|
+
def calc_wave_update(
|
|
106
|
+
self,
|
|
107
|
+
model_wave: NDArray[numpy.complexfloating],
|
|
108
|
+
model_intensity: NDArray[numpy.floating],
|
|
109
|
+
exp_patterns: NDArray[numpy.floating],
|
|
110
|
+
mask: NDArray[numpy.floating],
|
|
111
|
+
state: None
|
|
112
|
+
) -> t.Tuple[NDArray[numpy.complexfloating], None]:
|
|
113
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
import tifffile
|
|
8
|
+
|
|
9
|
+
from phaser.utils.num import to_numpy, abs2, fft2, get_array_module
|
|
10
|
+
from phaser.utils.image import remove_linear_ramp, colorize_complex, scale_to_integral_type
|
|
11
|
+
from phaser.utils.io import tiff_write_opts, tiff_write_opts_recip
|
|
12
|
+
from phaser.state import ReconsState
|
|
13
|
+
from phaser.plan import SaveOptions
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def output_images(state: ReconsState, out_dir: Path, options: SaveOptions):
|
|
17
|
+
for ty in options.images:
|
|
18
|
+
if ty not in _SAVE_FUNCS:
|
|
19
|
+
raise ValueError(f"Unknown image type '{ty}'")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
out_name = options.img_fmt.format(
|
|
23
|
+
type=ty, iter=state.iter,
|
|
24
|
+
)
|
|
25
|
+
out_path = out_dir / out_name
|
|
26
|
+
except KeyError as e:
|
|
27
|
+
raise ValueError(f"Invalid format string in 'img_fmt' (unknown key {e})") from None
|
|
28
|
+
except Exception as e:
|
|
29
|
+
raise ValueError("Invalid format string in 'img_fmt'") from e
|
|
30
|
+
|
|
31
|
+
_SAVE_FUNCS[ty](state, out_path, options)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def output_state(state: ReconsState, out_dir: Path, options: SaveOptions):
|
|
35
|
+
try:
|
|
36
|
+
out_name = options.hdf5_fmt.format(
|
|
37
|
+
iter=state.iter
|
|
38
|
+
)
|
|
39
|
+
except KeyError as e:
|
|
40
|
+
raise ValueError(f"Invalid format string in 'hdf5_fmt' (unknown key {e})") from None
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise ValueError("Invalid format string in 'hdf5_fmt'") from e
|
|
43
|
+
|
|
44
|
+
state.write_hdf5(out_dir / out_name)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _save_probe(state: ReconsState, out_path: Path, options: SaveOptions):
|
|
48
|
+
probe = to_numpy(state.probe.data)
|
|
49
|
+
write_opts = tiff_write_opts(state.probe.sampling, n_slices=probe.shape[0])
|
|
50
|
+
|
|
51
|
+
if options.img_dtype == 'float':
|
|
52
|
+
# save complex image
|
|
53
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
54
|
+
write_opts['metadata']['axes'] = 'CYX'
|
|
55
|
+
w.write(probe, **write_opts)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
img = scale_to_integral_type(
|
|
59
|
+
colorize_complex(probe), options.img_dtype
|
|
60
|
+
)
|
|
61
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
62
|
+
write_opts['metadata']['axes'] = 'CYXS'
|
|
63
|
+
w.write(img, photometric='rgb', **write_opts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _save_probe_mag(state: ReconsState, out_path: Path, options: SaveOptions):
|
|
67
|
+
probe_mag = abs2(state.probe.data)
|
|
68
|
+
write_opts = tiff_write_opts(state.probe.sampling, n_slices=probe_mag.shape[0])
|
|
69
|
+
|
|
70
|
+
if options.img_dtype != 'float':
|
|
71
|
+
probe_mag = scale_to_integral_type(to_numpy(probe_mag), options.img_dtype, min_range=0.2)
|
|
72
|
+
|
|
73
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
74
|
+
write_opts['metadata']['axes'] = 'CYX'
|
|
75
|
+
w.write(to_numpy(probe_mag), **write_opts)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _save_probe_recip(state: ReconsState, out_path: Path, options: SaveOptions):
|
|
79
|
+
xp = get_array_module(state.probe.data)
|
|
80
|
+
probe = to_numpy(xp.fft.fftshift(fft2(state.probe.data), axes=(-1, -2)))
|
|
81
|
+
write_opts = tiff_write_opts_recip(state.probe.sampling, n_slices=probe.shape[0])
|
|
82
|
+
|
|
83
|
+
if options.img_dtype == 'float':
|
|
84
|
+
# save complex image
|
|
85
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
86
|
+
write_opts['metadata']['axes'] = 'CYX'
|
|
87
|
+
w.write(probe, **write_opts)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
img = scale_to_integral_type(
|
|
91
|
+
colorize_complex(probe), options.img_dtype
|
|
92
|
+
)
|
|
93
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
94
|
+
write_opts['metadata']['axes'] = 'CYXS'
|
|
95
|
+
w.write(img, photometric='rgb', **write_opts)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _save_probe_recip_mag(state: ReconsState, out_path: Path, options: SaveOptions):
|
|
99
|
+
xp = get_array_module(state.probe.data)
|
|
100
|
+
probe_mag = to_numpy(abs2(xp.fft.fftshift(fft2(state.probe.data), axes=(-1, -2))))
|
|
101
|
+
write_opts = tiff_write_opts_recip(state.probe.sampling, n_slices=probe_mag.shape[0])
|
|
102
|
+
|
|
103
|
+
if options.img_dtype != 'float':
|
|
104
|
+
probe_mag = scale_to_integral_type(probe_mag, options.img_dtype, min_range=0.2)
|
|
105
|
+
|
|
106
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
107
|
+
write_opts['metadata']['axes'] = 'CYX'
|
|
108
|
+
w.write(probe_mag, **write_opts)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _save_object_phase(state: ReconsState, out_path: Path, options: SaveOptions, stack: bool = False):
|
|
112
|
+
crop = options.crop_roi
|
|
113
|
+
|
|
114
|
+
xp = get_array_module(state.object.data)
|
|
115
|
+
obj_phase = xp.angle(state.object.data)
|
|
116
|
+
|
|
117
|
+
obj_sampling = state.object.sampling
|
|
118
|
+
write_opts = tiff_write_opts(
|
|
119
|
+
obj_sampling,
|
|
120
|
+
corner=obj_sampling.region_min if crop else None,
|
|
121
|
+
zs=state.object.zs() if stack else None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if crop:
|
|
125
|
+
obj_phase = obj_phase[..., *state.object.sampling.get_region_crop()]
|
|
126
|
+
mask = xp.ones(obj_phase.shape[-2:], dtype=numpy.bool_)
|
|
127
|
+
else:
|
|
128
|
+
# include whole image, but only scale based on ROI
|
|
129
|
+
mask = state.object.sampling.get_region_mask(xp=xp)
|
|
130
|
+
|
|
131
|
+
if options.unwrap_phase:
|
|
132
|
+
obj_phase = xp.unwrap(xp.unwrap(obj_phase, axis=-1), axis=-2)
|
|
133
|
+
|
|
134
|
+
if not stack:
|
|
135
|
+
obj_phase = xp.sum(obj_phase, axis=0)
|
|
136
|
+
|
|
137
|
+
mask = to_numpy(mask)
|
|
138
|
+
obj_phase = remove_linear_ramp(to_numpy(obj_phase), mask)
|
|
139
|
+
|
|
140
|
+
if options.img_dtype != 'float':
|
|
141
|
+
obj_phase = scale_to_integral_type(obj_phase, options.img_dtype, mask)
|
|
142
|
+
|
|
143
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
144
|
+
write_opts['metadata']['axes'] = 'ZYX' if stack else 'YX'
|
|
145
|
+
w.write(obj_phase, **write_opts)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _save_object_mag(state: ReconsState, out_path: Path, options: SaveOptions, stack: bool = False):
|
|
149
|
+
crop = options.crop_roi
|
|
150
|
+
|
|
151
|
+
obj_sampling = state.object.sampling
|
|
152
|
+
write_opts = tiff_write_opts(
|
|
153
|
+
obj_sampling,
|
|
154
|
+
corner=obj_sampling.region_min if crop else None,
|
|
155
|
+
zs=state.object.zs() if stack else None,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
xp = get_array_module(state.object.data)
|
|
159
|
+
obj_mag = abs2(state.object.data)
|
|
160
|
+
if crop:
|
|
161
|
+
obj_mag = obj_mag[..., *state.object.sampling.get_region_crop()]
|
|
162
|
+
mask = numpy.ones(obj_mag.shape[-2:], dtype=numpy.bool_)
|
|
163
|
+
else:
|
|
164
|
+
# include whole image, but only scale based on ROI
|
|
165
|
+
mask = state.object.sampling.get_region_mask(xp=numpy)
|
|
166
|
+
|
|
167
|
+
if not stack:
|
|
168
|
+
obj_mag = xp.prod(obj_mag, axis=0)
|
|
169
|
+
|
|
170
|
+
obj_mag = to_numpy(obj_mag)
|
|
171
|
+
if options.img_dtype != 'float':
|
|
172
|
+
obj_mag = scale_to_integral_type(obj_mag, options.img_dtype, mask, min_range=0.2)
|
|
173
|
+
|
|
174
|
+
with tifffile.TiffWriter(out_path, ome=True) as w:
|
|
175
|
+
write_opts['metadata']['axes'] = 'ZYX' if stack else 'YX'
|
|
176
|
+
w.write(obj_mag, **write_opts)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
_SAVE_FUNCS: t.Dict[str, t.Callable[[ReconsState, Path, SaveOptions], t.Any]] = {
|
|
181
|
+
'probe': _save_probe,
|
|
182
|
+
'probe_mag': _save_probe_mag,
|
|
183
|
+
'probe_recip': _save_probe_recip,
|
|
184
|
+
'probe_recip_mag': _save_probe_recip_mag,
|
|
185
|
+
'object_phase_stack': partial(_save_object_phase, stack=True),
|
|
186
|
+
'object_phase_sum': partial(_save_object_phase, stack=False),
|
|
187
|
+
'object_mag_stack': partial(_save_object_mag, stack=True),
|
|
188
|
+
'object_mag_sum': partial(_save_object_mag, stack=False),
|
|
189
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
|
|
3
|
+
import numpy
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
|
|
6
|
+
from phaser.utils.num import get_array_module
|
|
7
|
+
from phaser.hooks.solver import (
|
|
8
|
+
PositionSolver, SteepestDescentPositionSolverProps, MomentumPositionSolverProps
|
|
9
|
+
)
|
|
10
|
+
from phaser.state import ReconsState
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SteepestDescentPositionSolver(PositionSolver[None]):
|
|
14
|
+
def __init__(self, args: None, props: SteepestDescentPositionSolverProps):
|
|
15
|
+
self.step_size = props.step_size
|
|
16
|
+
self.max_step_size = props.step_size
|
|
17
|
+
|
|
18
|
+
def init_state(self, sim: ReconsState) -> None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def perform_update(
|
|
22
|
+
self,
|
|
23
|
+
positions: NDArray[numpy.floating],
|
|
24
|
+
gradients: NDArray[numpy.floating],
|
|
25
|
+
state: None
|
|
26
|
+
) -> t.Tuple[NDArray[numpy.floating], None]:
|
|
27
|
+
xp = get_array_module(positions, gradients)
|
|
28
|
+
update = self.step_size * gradients
|
|
29
|
+
|
|
30
|
+
if self.max_step_size is not None:
|
|
31
|
+
update_mag = xp.linalg.norm(update, axis=-1, keepdims=True)
|
|
32
|
+
update *= xp.minimum(update_mag, self.max_step_size) / update_mag
|
|
33
|
+
|
|
34
|
+
return (update, state)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MomentumPositionSolver(PositionSolver[NDArray[numpy.floating]]):
|
|
38
|
+
def __init__(self, args: None, props: MomentumPositionSolverProps):
|
|
39
|
+
self.step_size = props.step_size
|
|
40
|
+
self.max_step_size = props.max_step_size
|
|
41
|
+
self.momentum = props.momentum
|
|
42
|
+
|
|
43
|
+
def init_state(self, sim: ReconsState) -> NDArray[numpy.floating]:
|
|
44
|
+
xp = get_array_module(sim.scan)
|
|
45
|
+
return xp.zeros_like(sim.scan)
|
|
46
|
+
|
|
47
|
+
def perform_update(
|
|
48
|
+
self,
|
|
49
|
+
positions: NDArray[numpy.floating],
|
|
50
|
+
gradients: NDArray[numpy.floating],
|
|
51
|
+
state: NDArray[numpy.floating]
|
|
52
|
+
) -> t.Tuple[NDArray[numpy.floating], NDArray[numpy.floating]]:
|
|
53
|
+
xp = get_array_module(positions, gradients, state)
|
|
54
|
+
|
|
55
|
+
update = self.step_size * gradients + self.momentum * state
|
|
56
|
+
|
|
57
|
+
if self.max_step_size is not None:
|
|
58
|
+
update_mag = xp.linalg.norm(update, axis=-1, keepdims=True)
|
|
59
|
+
update *= xp.minimum(update_mag, self.max_step_size) / update_mag
|
|
60
|
+
|
|
61
|
+
# state is just previous update step
|
|
62
|
+
return (update, update)
|