mirror of
https://github.com/cdtools-developers/cdtools.git
synced 2026-09-15 07:32:38 +02:00
Start documenting the models, and change the default FOV on fancy_ptycho reconstruction inspections
This commit is contained in:
Binary file not shown.
@@ -15,7 +15,7 @@ model.to(device='cuda')
|
||||
dataset.get_as(device='cuda')
|
||||
|
||||
# Now, we run a short reconstruction from the dataset
|
||||
for loss in model.Adam_optimize(100, dataset, batch_size=50, schedule=True):
|
||||
for loss in model.Adam_optimize(10, dataset, batch_size=50, schedule=True):
|
||||
# And we liveplot the updates to the model as they happen
|
||||
print(model.report())
|
||||
model.inspect(dataset)
|
||||
|
||||
@@ -16,11 +16,6 @@ def demo():
|
||||
model = cdtools.models.SimplePtycho.from_dataset(dataset)
|
||||
|
||||
|
||||
#class MyDataParallel(t.nn.DataParallel):
|
||||
# def __getattr__(self, name):
|
||||
# return getattr(self.module, name)
|
||||
|
||||
|
||||
print(dist.is_torchelastic_launched())
|
||||
dist.init_process_group(backend="nccl", init_method='env://')
|
||||
rank = dist.get_rank()
|
||||
@@ -29,7 +24,9 @@ def demo():
|
||||
print('device id', device_id)
|
||||
model = model.to(device_id)
|
||||
ddp_model = DDP(model, device_ids=[device_id])
|
||||
|
||||
print(model.loss_train)
|
||||
print(model.Adam_optimize)
|
||||
print(ddp_model.Adam_optimize)
|
||||
|
||||
sampler = torchdata.distributed.DistributedSampler(dataset)
|
||||
# Make a dataloader
|
||||
@@ -78,6 +75,8 @@ def demo():
|
||||
#loss = dist.all_reduce(loss)
|
||||
#print(it, 'time', time.time()-t0)
|
||||
#print(loss / normalization)
|
||||
if rank==0:
|
||||
model.inspect()
|
||||
return loss#.cpu().numpy()
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
@@ -14,6 +14,8 @@ dataset.patterns = t.cat([dataset.patterns]*5)
|
||||
# Next, we create a ptychography model from the dataset
|
||||
model = cdtools.models.SimplePtycho.from_dataset(dataset)
|
||||
|
||||
print(model.mask)
|
||||
exit()
|
||||
#class MyDataParallel(t.nn.DataParallel):
|
||||
# def __getattr__(self, name):
|
||||
# return getattr(self.module, name)
|
||||
|
||||
+135
-77
@@ -37,7 +37,6 @@ import numpy as np
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
from .complex_adam import MyAdam
|
||||
from .complex_lbfgs import MyLBFGS
|
||||
|
||||
__all__ = ['CDIModel']
|
||||
@@ -95,18 +94,52 @@ class CDIModel(t.nn.Module):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def store_detector_geometry(self, detector_geometry):
|
||||
if 'distance' in detector_geometry:
|
||||
def simulate_to_dataset(self, args_list):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def store_detector_geometry(self, detector_geometry, dtype=t.float32):
|
||||
"""Registers the information in a detector geometry dictionary
|
||||
|
||||
Information about the detector geometry is passed in as a dictionary,
|
||||
but we want the various properties to be registered as buffers in
|
||||
the model. This has nice effects, for example automatically updating
|
||||
with model.to, and making it possible to automatically save them out.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
detector_geometry : dict
|
||||
A dictionary containing at least the two entries 'distance' and 'basis'
|
||||
dtype : torch.dtype, default: torch.float32
|
||||
The datatype to convert the values to before registering
|
||||
"""
|
||||
self.register_buffer('det_basis',
|
||||
t.tensor(detector_geometry['basis'],
|
||||
dtype=dtype))
|
||||
|
||||
if 'distance' in detector_geometry \
|
||||
and detector_geometry['distance'] is not None:
|
||||
self.register_buffer('det_distance',
|
||||
t.as_tensor(detector_geometry['distance']))
|
||||
if 'basis' in detector_geometry:
|
||||
self.register_buffer('det_basis',
|
||||
t.as_tensor(detector_geometry['basis']))
|
||||
if 'corner' in detector_geometry:
|
||||
t.tensor(detector_geometry['distance'],
|
||||
dtype=dtype))
|
||||
if 'corner' in detector_geometry \
|
||||
and detector_geometry['corner'] is not None:
|
||||
self.register_buffer('det_corner',
|
||||
t.as_tensor(detector_geometry['corner']))
|
||||
t.tensor(detector_geometry['corner'],
|
||||
dtype=dtype))
|
||||
|
||||
def get_detector_geometry(self):
|
||||
"""Makes a detector geometry dictionary from the registered buffers
|
||||
|
||||
This extracts a dictionary with the detector geometry data from
|
||||
the registered buffers, helpful for functions which expect the
|
||||
geometry data to be in this format.
|
||||
|
||||
Returns
|
||||
-------
|
||||
detector_geometry : dict
|
||||
A dictionary containing at least the two entries 'distance' and 'basis', pulled from the model's buffers
|
||||
"""
|
||||
detector_geometry = {}
|
||||
if hasattr(self, 'det_distance'):
|
||||
detector_geometry['distance'] = self.det_distance
|
||||
@@ -116,9 +149,6 @@ class CDIModel(t.nn.Module):
|
||||
detector_geometry['corner'] = self.det_corner
|
||||
return detector_geometry
|
||||
|
||||
def simulate_to_dataset(self, args_list):
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_results(self):
|
||||
"""A convenience function to get the state dict as numpy arrays
|
||||
|
||||
@@ -130,29 +160,40 @@ class CDIModel(t.nn.Module):
|
||||
results of the reconstruction
|
||||
|
||||
Second, because display, further processing, long-term storage,
|
||||
etc. are often done with dictionaries of numpy files, it's useful
|
||||
etc. are often done with dictionaries of numpy arrays. So, it's useful
|
||||
to have a convenience function which does that conversion
|
||||
automatically.
|
||||
|
||||
Returns
|
||||
-------
|
||||
results : dict
|
||||
A dictionary containing all the parameters and buffers of the model, i.e. the result of self.state_dict(), converted to numpy.
|
||||
"""
|
||||
return {k: v.cpu().numpy() for k, v in self.state_dict().items()}
|
||||
|
||||
|
||||
def AD_optimize(self, iterations, data_loader, optimizer,\
|
||||
scheduler=None, regularization_factor=None, thread=True,
|
||||
calculation_width=10):
|
||||
"""Runs a round of reconstruction using the provided optimizer
|
||||
|
||||
This is the basic automatic differentiation reconstruction tool
|
||||
which all the other, algorithm-specific tools, use.
|
||||
which all the other, algorithm-specific tools, use. It is a
|
||||
generator which yields the average loss each epoch, ending after
|
||||
the specified number of iterations.
|
||||
|
||||
Like all the other optimization routines, it is defined as a
|
||||
generator function which yields the average loss each epoch.
|
||||
By default, the computation will be run in a separate thread. This
|
||||
is done to enable live plotting with matplotlib during a reconstruction.
|
||||
If the computation was done in the main thread, this would freeze
|
||||
the plots. This behavior can be turned off by setting the keyword
|
||||
argument 'thread' to False.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterations : int
|
||||
How many epochs of the algorithm to run
|
||||
dataset : CDataset
|
||||
The dataset to reconstruct against
|
||||
data_loader : torch.utils.data.DataLoader
|
||||
A data loader loading the CDataset to reconstruct
|
||||
optimizer : torch.optim.Optimizer
|
||||
The optimizer to run the reconstruction with
|
||||
scheduler : torch.optim.lr_scheduler._LRScheduler
|
||||
@@ -162,22 +203,34 @@ class CDIModel(t.nn.Module):
|
||||
thread : bool
|
||||
Default True, whether to run the computation in a separate thread to allow interaction with plots during computation
|
||||
calculation_width : int
|
||||
Default 10, how many translations to pass through at once for each round of gradient accumulation
|
||||
"""
|
||||
# First, calculate the normalization
|
||||
normalization = 0
|
||||
for inputs, patterns in data_loader:
|
||||
normalization += t.sum(patterns).cpu().numpy()
|
||||
Default 10, how many translations to pass through at once for each round of gradient accumulation. This does not affect the result, but may affect the calculation speed.
|
||||
|
||||
def run_iteration(stop_event=None):
|
||||
Yields
|
||||
------
|
||||
loss : float
|
||||
The summed loss over the latest epoch, divided by the total diffraction pattern intensity
|
||||
"""
|
||||
|
||||
def run_epoch(stop_event=None):
|
||||
"""Runs one full epoch of the reconstruction."""
|
||||
# First, initialize some tracking variables
|
||||
normalization = 0
|
||||
loss = 0
|
||||
N = 0
|
||||
t0 = time.time()
|
||||
|
||||
# The data loader is responsible for setting the minibatch
|
||||
# size, so each set is a minibatch
|
||||
for inputs, patterns in data_loader:
|
||||
normalization += t.sum(patterns).cpu().numpy()
|
||||
N += 1
|
||||
def closure():
|
||||
optimizer.zero_grad()
|
||||
|
||||
# We further break up the minibatch into a set of chunks.
|
||||
# This lets us use larger minibatches than can fit
|
||||
# on the GPU at once, while still doing batch processing
|
||||
# for efficiency
|
||||
input_chunks = [[inp[i:i + calculation_width]
|
||||
for inp in inputs]
|
||||
for i in range(0, len(inputs[0]),
|
||||
@@ -188,31 +241,39 @@ class CDIModel(t.nn.Module):
|
||||
|
||||
total_loss = 0
|
||||
for inp, pats in zip(input_chunks, pattern_chunks):
|
||||
# This is just used to allow graceful exit when
|
||||
# threading
|
||||
# This check allows for graceful exit when threading
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
exit()
|
||||
|
||||
# Run the simulation
|
||||
sim_patterns = self.forward(*inp)
|
||||
|
||||
# Calculate the loss
|
||||
if hasattr(self, 'mask'):
|
||||
loss = self.loss(pats,sim_patterns, mask=self.mask)
|
||||
else:
|
||||
loss = self.loss(pats,sim_patterns)
|
||||
|
||||
# And accumulate the gradients
|
||||
loss.backward()
|
||||
|
||||
total_loss += loss.detach()
|
||||
|
||||
# If we have a regularizer, we can calculate it separately,
|
||||
# and the gradients will add to the minibatch gradient
|
||||
if regularization_factor is not None \
|
||||
and hasattr(self, 'regularizer'):
|
||||
loss = self.regularizer(regularization_factor)
|
||||
loss.backward()
|
||||
|
||||
return total_loss
|
||||
|
||||
# This takes the step for this minibatch
|
||||
loss += optimizer.step(closure).detach().cpu().numpy()
|
||||
|
||||
|
||||
loss /= normalization
|
||||
|
||||
# We step the scheduler after the full epoch
|
||||
if scheduler is not None:
|
||||
scheduler.step(loss)
|
||||
|
||||
@@ -220,19 +281,26 @@ class CDIModel(t.nn.Module):
|
||||
self.latest_iteration_time = time.time() - t0
|
||||
return loss
|
||||
|
||||
if thread:
|
||||
# If we don't want to run in a different thread, this is easy
|
||||
if not thread:
|
||||
for it in range(iterations):
|
||||
yield run_iteration()
|
||||
|
||||
# But if we do want to thread, it's annoying:
|
||||
else:
|
||||
# Here we set up the communication with the computation thread
|
||||
result_queue = queue.Queue()
|
||||
stop_event = threading.Event()
|
||||
def target():
|
||||
try:
|
||||
result_queue.put(run_iteration(stop_event))
|
||||
result_queue.put(run_epoch(stop_event))
|
||||
except Exception as e:
|
||||
# If something bad happens, put the exception into the
|
||||
# result queue
|
||||
result_queue.put(e)
|
||||
|
||||
for it in range(iterations):
|
||||
if thread:
|
||||
# And this actually starts and monitors the thread
|
||||
for it in range(iterations):
|
||||
calc = threading.Thread(target=target, name='calculator', daemon=True)
|
||||
try:
|
||||
calc.start()
|
||||
@@ -256,9 +324,6 @@ class CDIModel(t.nn.Module):
|
||||
|
||||
yield res
|
||||
|
||||
else:
|
||||
yield run_iteration()
|
||||
|
||||
|
||||
def Adam_optimize(self, iterations, dataset, batch_size=15, lr=0.005,
|
||||
schedule=False, amsgrad=False, subset=None,
|
||||
@@ -280,7 +345,7 @@ class CDIModel(t.nn.Module):
|
||||
batch_size : int
|
||||
Optional, the size of the minibatches to use
|
||||
lr : float
|
||||
Optional, The learning rate (alpha) to use
|
||||
Optional, The learning rate (alpha) to use. 0.05 is typically the highest value with any chance of being stable
|
||||
schedule : float
|
||||
Optional, whether to use the ReduceLROnPlateau scheduler
|
||||
subset : list(int) or int
|
||||
@@ -290,24 +355,23 @@ class CDIModel(t.nn.Module):
|
||||
thread : bool
|
||||
Default True, whether to run the computation in a separate thread to allow interaction with plots during computation
|
||||
calculation_width : int
|
||||
Default 1, how many translations to pass through at once for each round of gradient accumulation
|
||||
Default 10, how many translations to pass through at once for each round of gradient accumulation. Does not affect the result, only the calculation speed
|
||||
|
||||
"""
|
||||
|
||||
if subset is not None:
|
||||
# if just one pattern, turn into a list for convenience
|
||||
# if subset is just one pattern, turn into a list for convenience
|
||||
if type(subset) == type(1):
|
||||
subset = [subset]
|
||||
dataset = torchdata.Subset(dataset, subset)
|
||||
|
||||
# Make a dataloader
|
||||
data_loader = torchdata.DataLoader(dataset, batch_size=batch_size,
|
||||
data_loader = torchdata.DataLoader(dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=True)
|
||||
|
||||
# Define the optimizer
|
||||
#optimizer = t.optim.Adam(self.parameters(), lr = lr, amsgrad=amsgrad)
|
||||
optimizer = MyAdam(self.parameters(), lr = lr, amsgrad=amsgrad)
|
||||
|
||||
optimizer = t.optim.Adam(self.parameters(), lr = lr, amsgrad=amsgrad)
|
||||
|
||||
# Define the scheduler
|
||||
if schedule:
|
||||
@@ -351,7 +415,7 @@ class CDIModel(t.nn.Module):
|
||||
regularization_factor : float or list(float)
|
||||
Optional, if the model has a regularizer defined, the set of parameters to pass the regularizer method
|
||||
thread : bool
|
||||
Default True, whether to run the computation in a separate thread to allow interaction with plots during computation
|
||||
Default True, whether to run the computation in a separate thread to allow interaction with plots during computation.
|
||||
|
||||
"""
|
||||
if subset is not None:
|
||||
@@ -382,7 +446,7 @@ class CDIModel(t.nn.Module):
|
||||
lr=0.01, momentum=0, dampening=0, weight_decay=0,
|
||||
nesterov=False, subset=None, regularization_factor=None,
|
||||
thread=True, calculation_width=10):
|
||||
"""Runs a round of reconstruction using the SGDoptimizer
|
||||
"""Runs a round of reconstruction using the SGD optimizer
|
||||
|
||||
This algorithm is often less stable that Adam, but it is simpler
|
||||
and is the basic workhorse of gradience descent.
|
||||
@@ -438,7 +502,7 @@ class CDIModel(t.nn.Module):
|
||||
|
||||
|
||||
def report(self):
|
||||
"""Returns a string informing on the latest reconstruction iteration
|
||||
"""Returns a string with info about the latest reconstruction iteration
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -463,7 +527,7 @@ class CDIModel(t.nn.Module):
|
||||
of plots, if one exists, and then redraw them. Otherwise, it will
|
||||
plot a new set, and any subsequent updates will update the new set
|
||||
|
||||
Optionally, a dataset can be passed, which then will plot any
|
||||
Optionally, a dataset can be passed, which will allow plotting of any
|
||||
registered plots which need to incorporate some information from
|
||||
the dataset (such as geometry or a comparison with measured data).
|
||||
|
||||
@@ -479,22 +543,11 @@ class CDIModel(t.nn.Module):
|
||||
----------
|
||||
dataset : CDataset
|
||||
Optional, a dataset matched to the model type
|
||||
update : bool
|
||||
Default True, whether to update existing plots or plot new ones
|
||||
update : bool, default: True
|
||||
Whether to update existing plots or plot new ones
|
||||
|
||||
"""
|
||||
|
||||
#print('base models inspect: checking the object')
|
||||
#a = self.obj.detach()
|
||||
#def saveobj(a, filename):
|
||||
# a = np.abs(a)
|
||||
# plt.imshow(a)
|
||||
# plt.savefig(filename)
|
||||
#f = ['base_a.png', 'base_b.png', 'base_c.png', 'base_d.png']
|
||||
#comp = [a[i, j, :, :] for i, j in zip([0, 0, 1, 1], [0, 1, 0, 1])]
|
||||
#for i in range(4):
|
||||
# saveobj(comp[i], f[i])
|
||||
|
||||
# We find or create all the figures
|
||||
first_update = False
|
||||
if update and hasattr(self, 'figs') and self.figs:
|
||||
figs = self.figs
|
||||
@@ -508,7 +561,8 @@ class CDIModel(t.nn.Module):
|
||||
|
||||
idx = 0
|
||||
for plots in self.plot_list:
|
||||
# If a conditional is included in the plot
|
||||
# If a conditional is included in the plot, we check whether
|
||||
# it is True
|
||||
try:
|
||||
if len(plots) >=3 and not plots[2](self):
|
||||
continue
|
||||
@@ -525,33 +579,36 @@ class CDIModel(t.nn.Module):
|
||||
else:
|
||||
fig = figs[idx]
|
||||
|
||||
try:
|
||||
|
||||
try: # We try just plotting using the simplest allowed signature
|
||||
plotter(self,fig)
|
||||
plt.title(name)
|
||||
|
||||
except TypeError as e:
|
||||
# TypeError implies it wanted another argument, i.e. a dataset
|
||||
if dataset is not None:
|
||||
try:
|
||||
plotter(self, fig, dataset)
|
||||
plt.title(name)
|
||||
|
||||
except (IndexError, KeyError, AttributeError, np.linalg.LinAlgError) as e:
|
||||
except Exception as e: # Don't raise errors: it's just plots
|
||||
pass
|
||||
|
||||
except (IndexError, KeyError, AttributeError, np.linalg.LinAlgError) as e:
|
||||
except Exception as e: # Don't raise errors, it's just a plot
|
||||
pass
|
||||
|
||||
idx += 1
|
||||
|
||||
if update:
|
||||
# This seems to update the figure without blocking.
|
||||
plt.draw()
|
||||
fig.canvas.start_event_loop(0.001)
|
||||
|
||||
if first_update:
|
||||
# But this is needed the first time the figures update, or
|
||||
# they won't get drawn at all
|
||||
plt.pause(0.05 * len(self.figs))
|
||||
|
||||
|
||||
def save_figures(self, prefix='', extension='.eps'):
|
||||
def save_figures(self, prefix='', extension='.pdf'):
|
||||
"""Saves all currently open inspection figures.
|
||||
|
||||
Note that this function is not very intelligent - so, for example,
|
||||
@@ -586,10 +643,18 @@ class CDIModel(t.nn.Module):
|
||||
def compare(self, dataset, logarithmic=False):
|
||||
"""Opens a tool for comparing simulated and measured diffraction patterns
|
||||
|
||||
This does what it says on the tin.
|
||||
|
||||
Also, I am very sorry, the implementation was done while I was
|
||||
possessed by Beezlebub - do not try to fix this, if it breaks just
|
||||
kill it and start from scratch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : CDataset
|
||||
A dataset containing the simulated diffraction patterns to compare against
|
||||
logarithmic : bool, default: False
|
||||
Whether to plot the diffraction on a logarithmic scale
|
||||
"""
|
||||
|
||||
fig, axes = plt.subplots(1,3,figsize=(12,5.3))
|
||||
@@ -598,13 +663,6 @@ class CDIModel(t.nn.Module):
|
||||
|
||||
|
||||
def update_colorbar(im):
|
||||
# If the update brought the colorbar out of whack
|
||||
# (say, from clicking back in the navbar)
|
||||
# Holy fuck this was annoying. Sorry future for how
|
||||
# crappy this solution is.
|
||||
#if not np.allclose(im.colorbar.ax.get_xlim(),
|
||||
# (np.min(im.get_array()),
|
||||
# np.max(im.get_array()))):
|
||||
if hasattr(im, 'norecurse') and im.norecurse:
|
||||
im.norecurse=False
|
||||
return
|
||||
@@ -617,9 +675,9 @@ class CDIModel(t.nn.Module):
|
||||
fig.pattern_idx = idx
|
||||
updating = True if len(axes[0].images) >= 1 else False
|
||||
|
||||
inputs, output = dataset[idx]
|
||||
sim_data = self.forward(*inputs).detach().cpu().numpy()
|
||||
meas_data = output.detach().cpu().numpy()
|
||||
inputs, output = dataset[idx:idx+1]
|
||||
sim_data = self.forward(*inputs).detach().cpu().numpy()[0]
|
||||
meas_data = output.detach().cpu().numpy()[0]
|
||||
if hasattr(self, 'mask') and self.mask is not None:
|
||||
mask = self.mask.detach().cpu().numpy()
|
||||
else:
|
||||
|
||||
@@ -72,7 +72,7 @@ class Bragg2DPtycho(CDIModel):
|
||||
background=None, translation_offsets=None, mask=None,
|
||||
weights=None, translation_scale=1, saturation=None,
|
||||
probe_support=None, oversampling=1,
|
||||
propagate_probe=True, correct_tilt=True, lens=False):
|
||||
propagate_probe=True, correct_tilt=True, lens=False, units='um'):
|
||||
|
||||
# We need the detector geometry
|
||||
# We need the probe basis (but in this case, we don't need the surface
|
||||
@@ -84,6 +84,7 @@ class Bragg2DPtycho(CDIModel):
|
||||
# propagate_probe and correct_tilt are important!
|
||||
|
||||
super(Bragg2DPtycho, self).__init__()
|
||||
self.units = units
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
det_geo = self.detector_geometry
|
||||
@@ -375,14 +376,15 @@ class Bragg2DPtycho(CDIModel):
|
||||
prs = Ws[...,None,None,None] * self.probe * self.probe_support[...,:,:]
|
||||
# Now we need to propagate each of the probes
|
||||
|
||||
|
||||
for j in range(prs.shape[0]):
|
||||
# I believe this -1 sign is in error, but I need a dataset with
|
||||
# well understood geometry to figure it out
|
||||
propagator = t.exp(
|
||||
1j*(props[j]*(2*np.pi)/self.wavelength)
|
||||
* self.universal_propagator)
|
||||
prs[j] = tools.propagators.near_field(prs[j], propagator)
|
||||
|
||||
if self.propagate_probe:
|
||||
for j in range(prs.shape[0]):
|
||||
# I believe this -1 sign is in error, but I need a dataset with
|
||||
# well understood geometry to figure it out
|
||||
propagator = t.exp(
|
||||
1j*(props[j]*(2*np.pi)/self.wavelength)
|
||||
* self.universal_propagator)
|
||||
prs[j] = tools.propagators.near_field(prs[j], propagator)
|
||||
|
||||
exit_waves = self.probe_norm * tools.interactions.ptycho_2D_sinc(
|
||||
prs, self.obj,pix_trans,
|
||||
@@ -495,20 +497,18 @@ class Bragg2DPtycho(CDIModel):
|
||||
|
||||
|
||||
plot_list = [
|
||||
('Dominant Probe Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe[0], fig=fig)),
|
||||
('Dominant Probe Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe[0], fig=fig)),
|
||||
('Subdominant Probe Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe[1], fig=fig),
|
||||
lambda self: len(self.probe) >=2),
|
||||
('Subdominant Probe Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe[1], fig=fig),
|
||||
lambda self: len(self.probe) >=2),
|
||||
('Basis Probe Fourier Space Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
('Basis Probe Fourier Space Phases',
|
||||
lambda self, fig: p.plot_phase(tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
('Basis Probe Real Space Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.probe, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Basis Probe Real Space Phases',
|
||||
lambda self, fig: p.plot_phase(self.probe, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig)),
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig, basis=self.probe_basis)),
|
||||
('Object Phase',
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig)),
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig, basis=self.probe_basis)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig)),
|
||||
('Background',
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
import math
|
||||
import torch
|
||||
from torch import Tensor
|
||||
from torch.optim.optimizer import Optimizer
|
||||
from typing import List, Optional
|
||||
|
||||
class MyAdam(Optimizer):
|
||||
r"""Implements Adam algorithm.
|
||||
It has been proposed in `Adam: A Method for Stochastic Optimization`_.
|
||||
The implementation of the L2 penalty follows changes proposed in
|
||||
`Decoupled Weight Decay Regularization`_.
|
||||
Args:
|
||||
params (iterable): iterable of parameters to optimize or dicts defining
|
||||
parameter groups
|
||||
lr (float, optional): learning rate (default: 1e-3)
|
||||
betas (Tuple[float, float], optional): coefficients used for computing
|
||||
running averages of gradient and its square (default: (0.9, 0.999))
|
||||
eps (float, optional): term added to the denominator to improve
|
||||
numerical stability (default: 1e-8)
|
||||
weight_decay (float, optional): weight decay (L2 penalty) (default: 0)
|
||||
amsgrad (boolean, optional): whether to use the AMSGrad variant of this
|
||||
algorithm from the paper `On the Convergence of Adam and Beyond`_
|
||||
(default: False)
|
||||
.. _Adam\: A Method for Stochastic Optimization:
|
||||
https://arxiv.org/abs/1412.6980
|
||||
.. _Decoupled Weight Decay Regularization:
|
||||
https://arxiv.org/abs/1711.05101
|
||||
.. _On the Convergence of Adam and Beyond:
|
||||
https://openreview.net/forum?id=ryQu7f-RZ
|
||||
"""
|
||||
|
||||
def __init__(self, params, lr=1e-3, betas=(0.9, 0.999), eps=1e-8,
|
||||
weight_decay=0, amsgrad=False):
|
||||
if not 0.0 <= lr:
|
||||
raise ValueError("Invalid learning rate: {}".format(lr))
|
||||
if not 0.0 <= eps:
|
||||
raise ValueError("Invalid epsilon value: {}".format(eps))
|
||||
if not 0.0 <= betas[0] < 1.0:
|
||||
raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0]))
|
||||
if not 0.0 <= betas[1] < 1.0:
|
||||
raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1]))
|
||||
if not 0.0 <= weight_decay:
|
||||
raise ValueError("Invalid weight_decay value: {}".format(weight_decay))
|
||||
defaults = dict(lr=lr, betas=betas, eps=eps,
|
||||
weight_decay=weight_decay, amsgrad=amsgrad)
|
||||
super(MyAdam, self).__init__(params, defaults)
|
||||
|
||||
def __setstate__(self, state):
|
||||
super(MyAdam, self).__setstate__(state)
|
||||
for group in self.param_groups:
|
||||
group.setdefault('amsgrad', False)
|
||||
|
||||
@torch.no_grad()
|
||||
def step(self, closure=None):
|
||||
"""Performs a single optimization step.
|
||||
Args:
|
||||
closure (callable, optional): A closure that reevaluates the model
|
||||
and returns the loss.
|
||||
"""
|
||||
loss = None
|
||||
if closure is not None:
|
||||
with torch.enable_grad():
|
||||
loss = closure()
|
||||
|
||||
for group in self.param_groups:
|
||||
params_with_grad = []
|
||||
grads = []
|
||||
exp_avgs = []
|
||||
exp_avg_sqs = []
|
||||
max_exp_avg_sqs = []
|
||||
state_steps = []
|
||||
beta1, beta2 = group['betas']
|
||||
|
||||
for p in group['params']:
|
||||
if p.grad is not None:
|
||||
params_with_grad.append(p)
|
||||
if p.grad.is_sparse:
|
||||
raise RuntimeError('Adam does not support sparse gradients, please consider SparseAdam instead')
|
||||
grads.append(p.grad)
|
||||
|
||||
state = self.state[p]
|
||||
# Lazy state initialization
|
||||
if len(state) == 0:
|
||||
state['step'] = 0
|
||||
# Exponential moving average of gradient values
|
||||
state['exp_avg'] = torch.zeros_like(p, memory_format=torch.preserve_format)
|
||||
# Exponential moving average of squared gradient values
|
||||
state['exp_avg_sq'] = torch.zeros_like(p, memory_format=torch.preserve_format)
|
||||
if group['amsgrad']:
|
||||
# Maintains max of all exp. moving avg. of sq. grad. values
|
||||
state['max_exp_avg_sq'] = torch.zeros_like(p, memory_format=torch.preserve_format)
|
||||
|
||||
exp_avgs.append(state['exp_avg'])
|
||||
exp_avg_sqs.append(state['exp_avg_sq'])
|
||||
|
||||
if group['amsgrad']:
|
||||
max_exp_avg_sqs.append(state['max_exp_avg_sq'])
|
||||
|
||||
# update the steps for each param group update
|
||||
state['step'] += 1
|
||||
# record the step after step update
|
||||
state_steps.append(state['step'])
|
||||
|
||||
adam(params_with_grad,
|
||||
grads,
|
||||
exp_avgs,
|
||||
exp_avg_sqs,
|
||||
max_exp_avg_sqs,
|
||||
state_steps,
|
||||
amsgrad=group['amsgrad'],
|
||||
beta1=beta1,
|
||||
beta2=beta2,
|
||||
lr=group['lr'],
|
||||
weight_decay=group['weight_decay'],
|
||||
eps=group['eps'])
|
||||
return loss
|
||||
|
||||
|
||||
|
||||
def adam(params: List[Tensor],
|
||||
grads: List[Tensor],
|
||||
exp_avgs: List[Tensor],
|
||||
exp_avg_sqs: List[Tensor],
|
||||
max_exp_avg_sqs: List[Tensor],
|
||||
state_steps: List[int],
|
||||
*,
|
||||
amsgrad: bool,
|
||||
beta1: float,
|
||||
beta2: float,
|
||||
lr: float,
|
||||
weight_decay: float,
|
||||
eps: float):
|
||||
r"""Functional API that performs Adam algorithm computation.
|
||||
See :class:`~torch.optim.Adam` for details.
|
||||
"""
|
||||
|
||||
for i, param in enumerate(params):
|
||||
|
||||
grad = grads[i]
|
||||
exp_avg = exp_avgs[i]
|
||||
exp_avg_sq = exp_avg_sqs[i]
|
||||
step = state_steps[i]
|
||||
|
||||
bias_correction1 = 1 - beta1 ** step
|
||||
bias_correction2 = 1 - beta2 ** step
|
||||
|
||||
if weight_decay != 0:
|
||||
grad = grad.add(param, alpha=weight_decay)
|
||||
|
||||
# Decay the first and second moment running average coefficient
|
||||
exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1)
|
||||
exp_avg_sq.mul_(beta2).addcmul_(grad, grad.conj(), value=1 - beta2)
|
||||
if amsgrad:
|
||||
# Maintains the maximum of all 2nd moment running avg. till now
|
||||
torch.maximum(max_exp_avg_sqs[i], exp_avg_sq, out=max_exp_avg_sqs[i])
|
||||
# Use the max. for normalizing running avg. of gradient
|
||||
denom = (max_exp_avg_sqs[i].sqrt() / math.sqrt(bias_correction2)).add_(eps)
|
||||
else:
|
||||
denom = (exp_avg_sq.sqrt() / math.sqrt(bias_correction2)).add_(eps)
|
||||
|
||||
step_size = lr / bias_correction1
|
||||
|
||||
param.addcdiv_(exp_avg, denom, value=-step_size)
|
||||
@@ -38,33 +38,40 @@ class FancyPtycho(CDIModel):
|
||||
units='um',
|
||||
simulate_probe_translation=False,
|
||||
simulate_finite_pixels=False,
|
||||
dtype=t.float32,
|
||||
obj_view_crop=0
|
||||
):
|
||||
|
||||
super(FancyPtycho, self).__init__()
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
det_geo = self.detector_geometry
|
||||
if 'distance' in det_geo:
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'], dtype=t.float32)
|
||||
if 'basis' in det_geo:
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'], dtype=t.float32)
|
||||
if 'corner' in det_geo and det_geo['corner'] is not None:
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'], dtype=t.float32)
|
||||
self.register_buffer('wavelength',
|
||||
t.tensor(wavelength, dtype=dtype))
|
||||
self.store_detector_geometry(detector_geometry,
|
||||
dtype=dtype)
|
||||
|
||||
self.min_translation = t.tensor(min_translation)
|
||||
self.register_buffer('min_translation',
|
||||
t.tensor(min_translation, dtype=dtype))
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.register_buffer('probe_basis',
|
||||
t.tensor(probe_basis, dtype=dtype))
|
||||
|
||||
self.detector_slice = copy(detector_slice)
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
self.register_buffer('surface_normal',
|
||||
t.tensor(surface_normal, dtype=dtype))
|
||||
if saturation is None:
|
||||
self.saturation = None
|
||||
else:
|
||||
self.register_buffer('saturation',
|
||||
t.tensor(saturation, dtype=dtype))
|
||||
# Not sure how to make this a buffer...
|
||||
self.units = units
|
||||
|
||||
self.fourier_probe = fourier_probe
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
self.mask = None
|
||||
else:
|
||||
self.mask = t.tensor(mask, dtype=t.bool)
|
||||
self.register_buffer('mask',
|
||||
t.tensor(mask, dtype=t.bool))
|
||||
|
||||
probe_guess = t.tensor(probe_guess, dtype=t.complex64)
|
||||
obj_guess = t.tensor(obj_guess, dtype=t.complex64)
|
||||
@@ -72,13 +79,18 @@ class FancyPtycho(CDIModel):
|
||||
# We rescale the probe here so it learns at the same rate as the
|
||||
# object
|
||||
if probe_guess.dim() > 2:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess[0]))
|
||||
probe_norm = 1 * t.max(t.abs(probe_guess[0]))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
self.register_buffer('probe_norm', probe_norm.to(dtype))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
|
||||
self.obj_view_slice = np.s_[obj_view_crop:-obj_view_crop,
|
||||
obj_view_crop:-obj_view_crop]
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
dummy_det = t.empty([s//oversampling
|
||||
@@ -112,22 +124,28 @@ class FancyPtycho(CDIModel):
|
||||
t_o = t_o / translation_scale
|
||||
self.translation_offsets = t.nn.Parameter(t_o)
|
||||
|
||||
self.translation_scale = translation_scale
|
||||
|
||||
if probe_support is not None:
|
||||
self.probe_support = probe_support
|
||||
else:
|
||||
self.probe_support = t.ones_like(self.probe[0], dtype=t.bool)
|
||||
self.register_buffer('translation_scale',
|
||||
t.tensor(translation_scale, dtype=dtype))
|
||||
|
||||
if probe_support is None:
|
||||
probe_support = t.ones_like(self.probe[0], dtype=t.bool)
|
||||
self.register_buffer('probe_support',
|
||||
t.tensor(probe_support, dtype=t.bool))
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
self.simulate_probe_translation = simulate_probe_translation
|
||||
|
||||
if simulate_probe_translation:
|
||||
Is = t.arange(self.probe.shape[-2], dtype=t.float32)
|
||||
Js = t.arange(self.probe.shape[-1], dtype=t.float32)
|
||||
Is = t.arange(self.probe.shape[-2], dtype=dtype)
|
||||
Js = t.arange(self.probe.shape[-1], dtype=dtype)
|
||||
Is, Js = t.meshgrid(Is/t.max(Is), Js/t.max(Js))
|
||||
self.I_phase = 2 * np.pi* Is * self.oversampling
|
||||
self.J_phase = 2 * np.pi* Js * self.oversampling
|
||||
|
||||
I_phase = 2 * np.pi* Is * self.oversampling
|
||||
J_phase = 2 * np.pi* Js * self.oversampling
|
||||
self.register_buffer('I_phase', I_phase)
|
||||
self.register_buffer('J_phase', J_phase)
|
||||
|
||||
|
||||
self.simulate_finite_pixels = simulate_finite_pixels
|
||||
|
||||
@@ -165,6 +183,8 @@ class FancyPtycho(CDIModel):
|
||||
units='um',
|
||||
simulate_probe_translation=False,
|
||||
simulate_finite_pixels=False,
|
||||
obj_view_crop=None,
|
||||
obj_padding=200
|
||||
):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
@@ -221,7 +241,7 @@ class FancyPtycho(CDIModel):
|
||||
|
||||
pix_translations = tools.interactions.translations_to_pixel(probe_basis, translations, surface_normal=surface_normal)
|
||||
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(probe_shape, pix_translations, padding=200)
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(probe_shape, pix_translations, padding=obj_padding)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
@@ -254,6 +274,12 @@ class FancyPtycho(CDIModel):
|
||||
if n_obj_modes != 1:
|
||||
obj = t.stack([obj,] + [0.05*t.ones_like(obj),]*(n_obj_modes-1))
|
||||
|
||||
if obj_view_crop is None:
|
||||
obj_view_crop = min(probe.shape[-2], probe.shape[-1]) // 2
|
||||
if obj_view_crop < 0:
|
||||
obj_view_crop += min(probe.shape[-2], probe.shape[-1]) // 2
|
||||
obj_view_crop += obj_padding
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset), 2)) - 0.5)
|
||||
@@ -312,7 +338,8 @@ class FancyPtycho(CDIModel):
|
||||
oversampling=oversampling,
|
||||
loss=loss, units=units,
|
||||
simulate_probe_translation=simulate_probe_translation,
|
||||
simulate_finite_pixels=simulate_finite_pixels)
|
||||
simulate_finite_pixels=simulate_finite_pixels,
|
||||
obj_view_crop=obj_view_crop)
|
||||
|
||||
|
||||
def interaction(self, index, translations, *args):
|
||||
@@ -338,7 +365,7 @@ class FancyPtycho(CDIModel):
|
||||
# For a Fourier-space probe, we take an IFT
|
||||
if self.fourier_probe:
|
||||
basis_prs = tools.propagators.inverse_far_field(basis_prs)
|
||||
|
||||
|
||||
# Now we construct the probes for each shot from the basis probes
|
||||
if self.weights is not None:
|
||||
Ws = self.weights[index]
|
||||
@@ -358,10 +385,10 @@ class FancyPtycho(CDIModel):
|
||||
# coherent mode index, then x,y, and then complex index
|
||||
# Maybe this can be done with a matmul now?
|
||||
prs = t.sum(Ws[..., None, None] * basis_prs, axis=-3)
|
||||
|
||||
|
||||
if self.simulate_probe_translation:
|
||||
det_pix_trans = tools.interactions.translations_to_pixel(
|
||||
self.detector_geometry['basis'],
|
||||
self.det_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
|
||||
@@ -375,8 +402,8 @@ class FancyPtycho(CDIModel):
|
||||
# which allows us to do stuff like let the object be super-resolution,
|
||||
# while restricting the probe to the detector resolution but still
|
||||
# doing an explicit real-space limitation of the probe
|
||||
padding = [self.background.shape[-2] - prs.shape[-2],
|
||||
self.background.shape[-1] - prs.shape[-1]]
|
||||
padding = [self.oversampling * self.background.shape[-2] - prs.shape[-2],
|
||||
self.oversampling * self.background.shape[-1] - prs.shape[-1]]
|
||||
|
||||
if any([p != 0 for p in padding]): # For probe_fourier_crop != 0.
|
||||
padding = [padding[-1]//2, padding[-1]-padding[-1]//2,
|
||||
@@ -415,32 +442,6 @@ class FancyPtycho(CDIModel):
|
||||
# Note: No "loss" function is defined here, because it is added
|
||||
# dynamically during object creation in __init__
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(FancyPtycho, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args, **kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if 'distance' in det_geo:
|
||||
det_geo['distance'] = det_geo['distance'].to(*args, **kwargs)
|
||||
if 'basis' in det_geo:
|
||||
det_geo['basis'] = det_geo['basis'].to(*args, **kwargs)
|
||||
if 'corner' in det_geo and det_geo['corner'] is not None:
|
||||
det_geo['corner'] = det_geo['corner'].to(*args, **kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
|
||||
if self.simulate_probe_translation:
|
||||
self.I_phase = self.I_phase.to(*args, **kwargs)
|
||||
self.J_phase = self.J_phase.to(*args, **kwargs)
|
||||
|
||||
self.min_translation = self.min_translation.to(*args, **kwargs)
|
||||
self.probe_basis = self.probe_basis.to(*args, **kwargs)
|
||||
self.probe_norm = self.probe_norm.to(*args, **kwargs)
|
||||
self.probe_support = self.probe_support.to(*args, **kwargs)
|
||||
self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
|
||||
def sim_to_dataset(self, args_list, calculation_width=None):
|
||||
# In the future, potentially add more control
|
||||
# over what metadata is saved (names, etc.)
|
||||
@@ -462,7 +463,6 @@ class FancyPtycho(CDIModel):
|
||||
'orientation': orientation}
|
||||
|
||||
|
||||
detector_geometry = self.detector_geometry
|
||||
mask = self.mask
|
||||
wavelength = self.wavelength
|
||||
indices, translations = args_list
|
||||
@@ -490,7 +490,7 @@ class FancyPtycho(CDIModel):
|
||||
entry_info=entry_info,
|
||||
sample_info=sample_info,
|
||||
wavelength=wavelength,
|
||||
detector_geometry=detector_geometry,
|
||||
detector_geometry=self.get_detector_geometry(),
|
||||
mask=mask)
|
||||
|
||||
|
||||
@@ -629,32 +629,77 @@ class FancyPtycho(CDIModel):
|
||||
|
||||
plot_list = [
|
||||
('',
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(dataset, fig=fig, mode='root_sum_intensity', image_title='Root Summed Probe Intensities', image_colorbar_title='Square Root of Intensity'),
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(
|
||||
dataset,
|
||||
fig=fig,
|
||||
mode='root_sum_intensity',
|
||||
image_title='Root Summed Probe Intensities',
|
||||
image_colorbar_title='Square Root of Intensity'),
|
||||
lambda self: len(self.weights.shape) >= 2),
|
||||
('',
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(dataset, fig=fig, mode='amplitude', image_title='Probe Amplitudes (scroll to view modes)', image_colorbar_title='Probe Amplitude'),
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(
|
||||
dataset,
|
||||
fig=fig,
|
||||
mode='amplitude',
|
||||
image_title='Probe Amplitudes (scroll to view modes)',
|
||||
image_colorbar_title='Probe Amplitude'),
|
||||
lambda self: len(self.weights.shape) >= 2),
|
||||
('',
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(dataset, fig=fig, mode='phase', image_title='Probe Phases (scroll to view modes)', image_colorbar_title='Probe Phase'),
|
||||
lambda self, fig, dataset: self.plot_wavefront_variation(
|
||||
dataset,
|
||||
fig=fig,
|
||||
mode='phase',
|
||||
image_title='Probe Phases (scroll to view modes)',
|
||||
image_colorbar_title='Probe Phase'),
|
||||
lambda self: len(self.weights.shape) >= 2),
|
||||
('Basis Probe Fourier Space Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.probe if self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
(self.probe if self.fourier_probe
|
||||
else tools.propagators.inverse_far_field(self.probe)),
|
||||
fig=fig)),
|
||||
('Basis Probe Fourier Space Phases',
|
||||
lambda self, fig: p.plot_phase(self.probe if self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
lambda self, fig: p.plot_phase(
|
||||
(self.probe if self.fourier_probe
|
||||
else tools.propagators.inverse_far_field(self.probe))
|
||||
, fig=fig)),
|
||||
('Basis Probe Real Space Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.probe if not self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
(self.probe if not self.fourier_probe
|
||||
else tools.propagators.inverse_far_field(self.probe)),
|
||||
fig=fig,
|
||||
basis=self.probe_basis,
|
||||
units=self.units)),
|
||||
('Basis Probe Real Space Phases',
|
||||
lambda self, fig: p.plot_phase(self.probe if not self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
lambda self, fig: p.plot_phase(
|
||||
(self.probe if not self.fourier_probe
|
||||
else tools.propagators.inverse_far_field(self.probe)),
|
||||
fig=fig,
|
||||
basis=self.probe_basis,
|
||||
units=self.units)),
|
||||
('Average Density Matrix Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(np.nanmean(np.abs(self.get_rhos()), axis=0), fig=fig),
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
np.nanmean(np.abs(self.get_rhos()), axis=0),
|
||||
fig=fig),
|
||||
lambda self: len(self.weights.shape) >= 2),
|
||||
('% Power in Top Mode (only accurate after tidy_probes)',
|
||||
lambda self, fig, dataset: p.plot_nanomap(self.corrected_translations(dataset), analysis.calc_top_mode_fraction(self.get_rhos()), fig=fig, units=self.units),
|
||||
lambda self, fig, dataset: p.plot_nanomap(
|
||||
self.corrected_translations(dataset),
|
||||
analysis.calc_top_mode_fraction(self.get_rhos()),
|
||||
fig=fig,
|
||||
units=self.units),
|
||||
lambda self: len(self.weights.shape) >= 2),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
self.obj[self.obj_view_slice],
|
||||
fig=fig,
|
||||
basis=self.probe_basis,
|
||||
units=self.units)),
|
||||
('Object Phase',
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
lambda self, fig: p.plot_phase(
|
||||
self.obj[self.obj_view_slice],
|
||||
fig=fig,
|
||||
basis=self.probe_basis,
|
||||
units=self.units)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig, units=self.units)),
|
||||
('Background',
|
||||
|
||||
@@ -45,22 +45,9 @@ class SimplePtycho(CDIModel):
|
||||
# object
|
||||
self.register_buffer('probe_norm', t.max(t.abs(probe_guess)))
|
||||
|
||||
#self.probe_data = complexWrapper(probe_guess/self.probe_norm)
|
||||
self.probe_data = t.nn.Parameter(t.view_as_real(probe_guess / self.probe_norm))
|
||||
self.obj_data = t.nn.Parameter(t.view_as_real(obj_guess))
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
# Do this for all the complex-valued parameters which are stored as real
|
||||
# valued for compatibility reasons. Note that updating model.probe.data
|
||||
# won't update the underlying storage, but something like:
|
||||
# model.probe.data[:] = ...
|
||||
# will update the underlying storage
|
||||
@property
|
||||
def probe(self):
|
||||
return t.view_as_complex(self.probe_data)
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
return t.view_as_complex(self.obj_data)
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset):
|
||||
@@ -94,8 +81,13 @@ class SimplePtycho(CDIModel):
|
||||
|
||||
# Next generate the object geometry from the probe geometry and
|
||||
# the translations
|
||||
pix_translations = tools.interactions.translations_to_pixel(probe_basis, translations, surface_normal=surface_normal)
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(probe_shape, pix_translations)
|
||||
pix_translations = tools.interactions.translations_to_pixel(
|
||||
probe_basis,
|
||||
translations,
|
||||
surface_normal=surface_normal)
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(
|
||||
probe_shape,
|
||||
pix_translations)
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice)
|
||||
@@ -109,17 +101,28 @@ class SimplePtycho(CDIModel):
|
||||
else:
|
||||
mask = None
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, det_slice, probe, obj, min_translation=min_translation, mask=mask, surface_normal=surface_normal)
|
||||
return cls(wavelength,
|
||||
det_geo,
|
||||
probe_basis,
|
||||
det_slice,
|
||||
probe,
|
||||
obj,
|
||||
min_translation=min_translation,
|
||||
mask=mask,
|
||||
surface_normal=surface_normal)
|
||||
|
||||
|
||||
def interaction(self, index, translations):
|
||||
pix_trans = tools.interactions.translations_to_pixel(self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans = tools.interactions.translations_to_pixel(
|
||||
self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
|
||||
pix_trans -= self.min_translation
|
||||
return tools.interactions.ptycho_2D_round(self.probe_norm * self.probe,
|
||||
self.obj,
|
||||
pix_trans)
|
||||
return tools.interactions.ptycho_2D_round(
|
||||
self.probe_norm * self.probe,
|
||||
self.obj,
|
||||
pix_trans)
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
@@ -133,50 +136,12 @@ class SimplePtycho(CDIModel):
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.intensity(wavefields,
|
||||
detector_slice=self.detector_slice)
|
||||
|
||||
|
||||
|
||||
def loss(self, real_data, sim_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
|
||||
|
||||
def sim_to_dataset(self, args_list):
|
||||
# In the future, potentially add more control
|
||||
# over what metadata is saved (names, etc.)
|
||||
|
||||
# First, I need to gather all the relevant data
|
||||
# that needs to be added to the dataset
|
||||
entry_info = {'program_name': 'cdtools',
|
||||
'instrument_n': 'Simulated Data',
|
||||
'start_time': datetime.now()}
|
||||
|
||||
surface_normal = self.surface_normal.detach().cpu().numpy()
|
||||
xsurfacevec = np.cross(np.array([0.,1.,0.]), surface_normal)
|
||||
xsurfacevec /= np.linalg.norm(xsurfacevec)
|
||||
ysurfacevec = np.cross(surface_normal, xsurfacevec)
|
||||
ysurfacevec /= np.linalg.norm(ysurfacevec)
|
||||
orientation = np.array([xsurfacevec, ysurfacevec, surface_normal])
|
||||
|
||||
sample_info = {'description': 'A simulated sample',
|
||||
'orientation': orientation}
|
||||
|
||||
detector_geometry = self.detector_geometry
|
||||
mask = self.mask
|
||||
wavelength = self.wavelength
|
||||
indices, translations = args_list
|
||||
|
||||
# Then we simulate the results
|
||||
data = self.forward(indices, translations)
|
||||
|
||||
# And finally, we make the dataset
|
||||
return Ptycho2DDataset(translations, data,
|
||||
entry_info = entry_info,
|
||||
sample_info = sample_info,
|
||||
wavelength=wavelength,
|
||||
detector_geometry=detector_geometry,
|
||||
mask=mask)
|
||||
|
||||
|
||||
|
||||
plot_list = [
|
||||
('Probe Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe, fig=fig, basis=self.probe_basis)),
|
||||
@@ -188,65 +153,3 @@ class SimplePtycho(CDIModel):
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig, basis=self.probe_basis))
|
||||
]
|
||||
|
||||
|
||||
def ePIE(self, iterations, dataset, beta = 1.0):
|
||||
"""Runs an ePIE reconstruction as described in `Maiden et al. (2017) <https://www.osapublishing.org/optica/abstract.cfm?uri=optica-4-7-736>`_.
|
||||
Optional parameters are:
|
||||
|
||||
:arg ``iterations``: Controls the number of iterations run, defaults to 1.
|
||||
:arg ``beta``: Algorithmic parameter described in Maiden's implementation of rPIE. Defaults to 0.15.
|
||||
:arg ``probe``: Initial probe wavefunction.
|
||||
:arg ``object``: Initial object wavefunction.
|
||||
"""
|
||||
probe_shape = self.probe.shape
|
||||
|
||||
if self.mask is not None:
|
||||
mask = self.mask[...,None]
|
||||
else:
|
||||
mask=None
|
||||
|
||||
def probe_update(exit_wave, exit_wave_corrected, probe, object, translation):
|
||||
new_probe = probe + tools.cmath.cmult(beta * tools.cmath.cconj(object[translation])/(self.probe_norm*t.max(tools.cmath.cabssq(object))), exit_wave_corrected-exit_wave)
|
||||
return new_probe
|
||||
|
||||
def object_update(exit_wave, exit_wave_corrected, probe, object, translation):
|
||||
new_object = object.clone()
|
||||
new_object[translation] = object[translation] + tools.cmath.cmult(beta * tools.cmath.cconj(probe)/(self.probe_norm*t.max(tools.cmath.cabssq(probe))), exit_wave_corrected-exit_wave)
|
||||
return new_object
|
||||
|
||||
with t.no_grad():
|
||||
data_loader = torchdata.DataLoader(dataset, shuffle=True)
|
||||
|
||||
for it in range(iterations):
|
||||
loss = []
|
||||
for (i, [translations]), [patterns] in data_loader:
|
||||
probe = self.probe.data.clone()
|
||||
object = self.obj.data.clone()
|
||||
|
||||
exit_wave = self.interaction(i, translations).clone()
|
||||
# Apply modulus constraint
|
||||
exit_wave_corrected = exit_wave.clone()
|
||||
exit_wave_corrected = self.forward_propagator(exit_wave_corrected.clone())
|
||||
exit_wave_corrected[self.detector_slice] = tools.projectors.modulus(exit_wave_corrected.clone()[self.detector_slice], patterns, mask = mask)
|
||||
exit_wave_corrected = self.backward_propagator(exit_wave_corrected.clone())
|
||||
|
||||
# Calculate the section of the object wavefunction to be modified
|
||||
pix_trans = tools.interactions.translations_to_pixel(self.probe_basis,
|
||||
translations)
|
||||
pix_trans -= self.min_translation
|
||||
|
||||
pix_trans = t.round(pix_trans).to(dtype=t.int32).detach().cpu().numpy()
|
||||
|
||||
object_slice = np.s_[pix_trans[0]:
|
||||
pix_trans[0]+probe_shape[0],
|
||||
pix_trans[1]:
|
||||
pix_trans[1]+probe_shape[1]]
|
||||
|
||||
# Apply probe and object updates
|
||||
self.probe.data = probe_update(exit_wave, exit_wave_corrected, probe, object, object_slice)
|
||||
self.obj.data = object_update(exit_wave, exit_wave_corrected, probe, object, object_slice)
|
||||
|
||||
# Calculate loss
|
||||
loss.append(self.loss(self.measurement(self.interaction(i, translations)), patterns))
|
||||
|
||||
yield t.mean(t.tensor(loss)).cpu().numpy()
|
||||
|
||||
@@ -95,8 +95,8 @@ def pixel_to_translations(basis, pixel_translations, surface_normal=t.Tensor([0,
|
||||
The real space basis the wavefields are defined in
|
||||
translations : torch.Tensor
|
||||
A Jx2 stack of pixel-space translations, or a single translation
|
||||
surface_normal : torch.Tensor
|
||||
Optional, the sample's surface normal
|
||||
surface_normal : torch.Tensor, default: torch.Tensor([0,0,1])
|
||||
The sample's surface normal
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
@@ -148,7 +148,7 @@ def incoherent_sum(wavefields, detector_slice=None, epsilon=1e-7, saturation=Non
|
||||
output = output[detector_slice]
|
||||
else:
|
||||
output = output[(np.s_[:],) + detector_slice]
|
||||
|
||||
|
||||
if saturation is None:
|
||||
return output + epsilon
|
||||
else:
|
||||
@@ -193,7 +193,7 @@ def quadratic_background(wavefield, background, *args, detector_slice=None, meas
|
||||
output = measurement(wavefield, *args, detector_slice=detector_slice,
|
||||
epsilon=epsilon, oversampling=oversampling,
|
||||
simulate_finite_pixels=simulate_finite_pixels) \
|
||||
+ background**2
|
||||
+ background**2
|
||||
|
||||
# This has to be done after the background is added, hence we replicate
|
||||
# it here
|
||||
|
||||
Reference in New Issue
Block a user