mirror of
https://github.com/cdtools-developers/cdtools.git
synced 2026-10-13 19:40:26 +02:00
move to a src directory layout
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# This is needed to allow us to use torch.tensor in the module without it
|
||||
# constantly complaining.
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore",
|
||||
message='To copy construct from a tensor, ')
|
||||
|
||||
__all__ = ['tools', 'datasets', 'models']
|
||||
|
||||
from CDTools import tools
|
||||
from CDTools import datasets
|
||||
from CDTools import models
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
""" This module contains all the datasets for interacting with ptychography data
|
||||
|
||||
All the access to data from standard ptychography and CDI experiments is
|
||||
coordinated through the various datasets defined in this module. They make use
|
||||
of the lower-level data reading and writing functions defined in tools.data,
|
||||
but critically all of these datasets subclass torch.Dataset. This allows
|
||||
them to be used as standard torch datasets during reconstructions, which
|
||||
helps make it easy to use the various data-handling strategies that are
|
||||
implemented by default in pytorch (such as drawing data in a random order,
|
||||
drawing minibatches, etc.)
|
||||
|
||||
New Datasets can be defined a subclass of the main CDataset class defined
|
||||
in the base.py file. Example implementations of all these functions
|
||||
can be found in the code for the Ptycho2DDataset class. In addition, it is
|
||||
recommended to read through the tutorial section on defining a new CDI
|
||||
dataset before attempting to do so
|
||||
|
||||
* __init__
|
||||
* __len__
|
||||
* _load
|
||||
* to
|
||||
* from_cxi
|
||||
* to_cxi
|
||||
* inspect
|
||||
|
||||
"""
|
||||
|
||||
# I don't believe that __all__ really needed, but it's nice to define it
|
||||
# to be explicit that import * is safe
|
||||
__all__ = ['CDataset','Ptycho2DDataset','PolarizedPtycho2DDataset']
|
||||
|
||||
from CDTools.datasets.base import CDataset
|
||||
from CDTools.datasets.ptycho_2d_dataset import Ptycho2DDataset
|
||||
from CDTools.datasets.polarized_ptycho_2d_dataset import PolarizedPtycho2DDataset
|
||||
@@ -0,0 +1,252 @@
|
||||
""" This module contains the base CDataset class for handling CDI data
|
||||
|
||||
Subclasses of CDataset are required to define their own implementations
|
||||
of the following functions:
|
||||
|
||||
* __init__
|
||||
* __len__
|
||||
* _load
|
||||
* to
|
||||
* from_cxi
|
||||
* to_cxi
|
||||
* inspect
|
||||
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
from copy import copy
|
||||
import h5py
|
||||
import pathlib
|
||||
from CDTools.tools import data as cdtdata
|
||||
from torch.utils import data as torchdata
|
||||
|
||||
__all__ = ['CDataset']
|
||||
|
||||
|
||||
class CDataset(torchdata.Dataset):
|
||||
""" The base dataset class which all other datasets subclass
|
||||
|
||||
Subclasses torch.utils.data.Dataset
|
||||
|
||||
This base dataset class defines the functionality which should be
|
||||
common to all subclassed datasets. This includes the loading and
|
||||
storage of the metadata portions of .cxi files, as well as the tools
|
||||
needed to allow for easy mixing of data on the CPU and GPU.
|
||||
"""
|
||||
|
||||
def __init__(self, entry_info=None, sample_info=None,
|
||||
wavelength=None,
|
||||
detector_geometry=None, mask=None,
|
||||
background=None):
|
||||
|
||||
"""The __init__ function allows construction from python objects.
|
||||
|
||||
The detector_geometry dictionary is defined to have the
|
||||
entries defined by the outputs of data.get_detector_geometry.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
entry_info : dict
|
||||
A dictionary containing the entry_info metadata
|
||||
sample_info : dict
|
||||
A dictionary containing the sample_info metadata
|
||||
wavelength : float
|
||||
The wavelength of light used in the experiment
|
||||
detector_geometry : dict
|
||||
A dictionary containing the various detector geometry
|
||||
parameters
|
||||
mask : array
|
||||
A mask for the detector, defined as 1 for live pixels, 0
|
||||
for dead
|
||||
background : array
|
||||
An initial guess for the not-previously-subtracted
|
||||
detector background
|
||||
"""
|
||||
|
||||
# Force pass-by-value-like behavior to stop strangeness
|
||||
self.entry_info = copy(entry_info)
|
||||
self.sample_info = copy(sample_info)
|
||||
self.wavelength = wavelength
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
if mask is not None:
|
||||
self.mask = t.tensor(mask, dtype=t.bool)
|
||||
else:
|
||||
self.mask = None
|
||||
if background is not None:
|
||||
self.background = t.tensor(background, dtype=t.float32)
|
||||
else:
|
||||
self.background = None
|
||||
|
||||
self.get_as(device='cpu')
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
"""Sends the relevant data to the given device and dtype
|
||||
|
||||
This function sends the stored mask and background to the
|
||||
specified device and dtype
|
||||
|
||||
Accepts the same parameters as torch.Tensor.to
|
||||
"""
|
||||
# The mask should always stay a uint8, but it should switch devices
|
||||
mask_kwargs = copy(kwargs)
|
||||
try:
|
||||
mask_kwargs.pop('dtype')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args,**mask_kwargs)
|
||||
if self.background is not None:
|
||||
self.background = self.background.to(*args,**kwargs)
|
||||
|
||||
|
||||
def get_as(self, *args, **kwargs):
|
||||
"""Sets the dataset to return data on the given device and dtype
|
||||
|
||||
Oftentimes there isn't room to store an entire dataset on a GPU,
|
||||
but it is still worth running the calculation on the GPU even with
|
||||
the overhead incurred by transferring data back and forth. In that
|
||||
case, get_as can be used instead of to, to declare a set of
|
||||
device and dtype that the data should be returned as, whenever it
|
||||
is accessed through the __getitem__ function (as it would be in
|
||||
any reconstructions).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
Accepts the same parameters as torch.Tensor.to
|
||||
"""
|
||||
self.get_as_args = (args, kwargs)
|
||||
|
||||
def __len__(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __getitem__(self, index):
|
||||
# Deals with loading to appropriate device/dtype, if
|
||||
# specified via a call to get_as
|
||||
inputs, outputs = self._load(index)
|
||||
if hasattr(self, 'get_as_args'):
|
||||
outputs = outputs.to(*self.get_as_args[0],**self.get_as_args[1])
|
||||
moved_inputs = []
|
||||
for inp in inputs:
|
||||
try:
|
||||
moved_inputs.append(inp.to(*self.get_as_args[0],**self.get_as_args[1]) )
|
||||
except:
|
||||
moved_inputs.append(inp)
|
||||
else:
|
||||
moved_inputs = inputs
|
||||
return moved_inputs, outputs
|
||||
|
||||
|
||||
def _load(self, index):
|
||||
""" Internal function to load data
|
||||
|
||||
In all subclasses of CDataset, a _load function should be defined.
|
||||
This function is used internally by the global __getitem__ function
|
||||
defined in the base class, which handles moving data around when
|
||||
the dataset is (for example) storing the data on the CPU but
|
||||
getting data as GPU tensors.
|
||||
|
||||
It should accept an index or slice, and return output as a tuple.
|
||||
The first item of the tuple is a tuple containing the inputs to
|
||||
the forward model for the related ptychography model. The second
|
||||
item of the tuple should be the set of diffraction patterns
|
||||
associated with the returned inputs.
|
||||
|
||||
Since there is no kind of data stored in a CDataset, this
|
||||
function is defined as returing a NotImplemented Error
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_cxi(cls, cxi_file):
|
||||
"""Generates a new CDataset from a .cxi file directly
|
||||
|
||||
This is the most commonly used constructor for CDatasets and
|
||||
subclasses thereof. It populates the dataset using the information
|
||||
in a .cxi file. It can either take an h5py.File object directly,
|
||||
or a filename or pathlib object pointing to the file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to load from
|
||||
|
||||
Returns
|
||||
-------
|
||||
dataset : CDataset
|
||||
The constructed dataset object
|
||||
"""
|
||||
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with h5py.File(cxi_file,'r') as f:
|
||||
return cls.from_cxi(f)
|
||||
|
||||
entry_info = cdtdata.get_entry_info(cxi_file)
|
||||
sample_info = cdtdata.get_sample_info(cxi_file)
|
||||
wavelength = cdtdata.get_wavelength(cxi_file)
|
||||
distance, basis, corner = cdtdata.get_detector_geometry(cxi_file)
|
||||
detector_geometry = {'distance' : distance,
|
||||
'basis' : basis,
|
||||
'corner' : corner}
|
||||
mask = cdtdata.get_mask(cxi_file)
|
||||
dark = cdtdata.get_dark(cxi_file)
|
||||
return cls(entry_info = entry_info,
|
||||
sample_info = sample_info,
|
||||
wavelength=wavelength,
|
||||
detector_geometry=detector_geometry,
|
||||
mask=mask, background=dark)
|
||||
|
||||
|
||||
def to_cxi(self, cxi_file):
|
||||
"""Saves out a CDataset as a .cxi file
|
||||
|
||||
This function saves all the compatible information in a CDataset
|
||||
object into a .cxi file. This is useful for saving out modified
|
||||
or simulated datasets
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to write to
|
||||
"""
|
||||
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with h5py.File(cxi_file,'w') as f:
|
||||
return self.to_cxi(f)
|
||||
|
||||
if self.entry_info is not None:
|
||||
cdtdata.add_entry_info(cxi_file, self.entry_info)
|
||||
if self.sample_info is not None:
|
||||
cdtdata.add_sample_info(cxi_file, self.sample_info)
|
||||
if self.wavelength is not None:
|
||||
cdtdata.add_source(cxi_file, self.wavelength)
|
||||
if self.detector_geometry is not None:
|
||||
if 'corner' in self.detector_geometry:
|
||||
corner = self.detector_geometry['corner']
|
||||
else:
|
||||
corner = None
|
||||
cdtdata.add_detector(cxi_file,
|
||||
self.detector_geometry['distance'],
|
||||
self.detector_geometry['basis'],
|
||||
corner = corner)
|
||||
if self.mask is not None:
|
||||
cdtdata.add_mask(cxi_file, self.mask)
|
||||
if self.background is not None:
|
||||
cdtdata.add_dark(cxi_file, self.background)
|
||||
|
||||
def inspect(self):
|
||||
"""The prototype for the inspect function
|
||||
|
||||
In all subclasses of CDataset, an inspect function should be
|
||||
defined which opens a tool that shows the data in a natural
|
||||
layout for that kind of experiment. In the base class, no actual
|
||||
data is stored, so this is defined to raise a NotImplementedError
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import numpy as np
|
||||
import torch as t
|
||||
import h5py
|
||||
import pathlib
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools.tools import data as cdtdata
|
||||
from CDTools.tools import plotting
|
||||
|
||||
__all__ = ['PolarizedPtycho2DDataset']
|
||||
|
||||
|
||||
class PolarizedPtycho2DDataset(Ptycho2DDataset):
|
||||
"""The standard dataset for a 2D ptychography scan
|
||||
|
||||
Subclasses datasets.Ptycho2DDataset
|
||||
|
||||
This class loads and saves 2D ptychography scan data from .cxi files.
|
||||
It should save and load files compatible with most reconstruction
|
||||
programs, although it is only tested against SHARP.
|
||||
"""
|
||||
|
||||
def __init__(self, translations, polarizer_angles, analyzer_angles, patterns, axes=None, *args, **kwargs):
|
||||
"""The __init__ function allows construction from python objects.
|
||||
|
||||
The detector_geometry dictionary is defined to have the
|
||||
entries defined by the outputs of data.get_detector_geometry.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
translations : array (nx3 t.tensor when polarized=True)
|
||||
An nx3 array containing the probe translations at each scan point
|
||||
patterns : array
|
||||
An nxmxl array containing the full stack of measured diffraction patterns
|
||||
axes : list(str)
|
||||
A
|
||||
of names for the axes of the probe translations
|
||||
entry_info : dict
|
||||
A dictionary containing the entry_info metadata
|
||||
sample_info : dict
|
||||
A dictionary containing the sample_info metadata
|
||||
wavelength : float
|
||||
The wavelength of light used in the experiment
|
||||
detector_geometry : dict
|
||||
A dictionary containing the various detector geometry
|
||||
parameters
|
||||
mask : array
|
||||
A mask for the detector, defined as 1 for live pixels, 0
|
||||
for dead
|
||||
background : array
|
||||
An initial guess for the not-previously-subtracted
|
||||
detector background
|
||||
"""
|
||||
|
||||
super(PolarizedPtycho2DDataset,self).__init__(translations, patterns,
|
||||
*args, **kwargs)
|
||||
|
||||
|
||||
self.polarizer = t.tensor(polarizer_angles, dtype=t.float32)
|
||||
self.analyzer = t.tensor(analyzer_angles, dtype=t.float32)
|
||||
|
||||
|
||||
def _load(self, index):
|
||||
""" Internal function to load data
|
||||
|
||||
This function is used internally by the global __getitem__ function
|
||||
defined in the base class, which handles moving data around when
|
||||
the dataset is (for example) storing the data on the CPU but
|
||||
getting data as GPU tensors.
|
||||
|
||||
It loads data in the format (inputs, output)
|
||||
|
||||
The inputs for a 2D ptychogaphy data set are:
|
||||
|
||||
1) The indices of the patterns to use
|
||||
2) The recorded probe positions associated with those points
|
||||
3) The angles of the polarizers if polarized=True
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index (polarized=False): int or slice
|
||||
The index or indices of the scan points to use
|
||||
|
||||
index (polarized=True):
|
||||
tuple ((phi1, phi2), ind)
|
||||
|
||||
ind - index or indices of the scan points to use (in a (phi1, phi2) polarization state), int or slice
|
||||
(phi1, phi2) - angles of the 1st and 2nd polarizers, ints
|
||||
|
||||
Returns
|
||||
-------
|
||||
inputs : tuple
|
||||
A tuple of the inputs to the related forward models
|
||||
if polarized: inputs = ((phi1, phi2), ind, transl[(phi1, phi2)][ind])
|
||||
outputs : tuple
|
||||
The output pattern or stack of output patterns
|
||||
"""
|
||||
return ((index, self.translations[index],
|
||||
self.polarizer[index], self.analyzer[index]),
|
||||
self.patterns[index])
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
"""Sends the relevant data to the given device and dtype
|
||||
|
||||
This function sends the stored translations, patterns,
|
||||
mask and background to the specified device and dtype
|
||||
|
||||
Accepts the same parameters as torch.Tensor.to
|
||||
"""
|
||||
super(PolarizedPtycho2DDataset, self).to(*args, **kwargs)
|
||||
self.polarizer = self.polarizer.to(*args, **kwargs)
|
||||
self.analyzer = self.analyzer.to(*args, **kwargs)
|
||||
|
||||
|
||||
# It sucks that I can't reuse the base factory method here,
|
||||
# perhaps there is a way but I couldn't figure it out.
|
||||
@classmethod
|
||||
def from_cxi(cls, cxi_file, cut_zeros=True):
|
||||
"""Generates a new PolarizedPtycho2DDataset from a .cxi file directly
|
||||
|
||||
This generates a new PolarizedPtycho2DDataset from a .cxi file storing
|
||||
a 2D ptychography scan.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to load from
|
||||
cut_zeros : bool
|
||||
Default True, whether to set all negative data to zero
|
||||
|
||||
Returns
|
||||
-------
|
||||
dataset : PolarizedPtycho2DDataset
|
||||
The constructed dataset object
|
||||
"""
|
||||
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with h5py.File(cxi_file, 'r') as f:
|
||||
return cls.from_cxi(f, cut_zeros=cut_zeros)
|
||||
|
||||
# Generate a base dataset
|
||||
dataset = Ptycho2DDataset.from_cxi(cxi_file, cut_zeros=cut_zeros)
|
||||
|
||||
# Mutate the class to this subclass (PolarizedPtycho2DDataset)
|
||||
dataset.__class__ = cls
|
||||
|
||||
# Now, we save out the polarizer and analyzer states
|
||||
polarizer = cdtdata.get_shot_to_shot_info(cxi_file, 'polarizer_angle')
|
||||
analyzer = cdtdata.get_shot_to_shot_info(cxi_file, 'analyzer_angle')
|
||||
|
||||
dataset.analyzer = t.tensor(analyzer, dtype=t.float32)
|
||||
dataset.polarizer = t.tensor(polarizer, dtype=t.float32)
|
||||
|
||||
return dataset
|
||||
|
||||
def to_cxi(self, cxi_file, polarized=False):
|
||||
"""Saves out a PolarizedPtycho2DDataset as a .cxi file
|
||||
|
||||
This function saves all the compatible information in a
|
||||
PolarizedPtycho2DDataset object into a .cxi file. This saved .cxi file
|
||||
should be compatible with any standard .cxi file based
|
||||
reconstruction tool, such as SHARP.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to write to
|
||||
"""
|
||||
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with cdtdata.create_cxi(cxi_file) as f:
|
||||
return self.to_cxi(f)
|
||||
|
||||
# This saves the translations, patterns, etc.
|
||||
super(PolarizedPtycho2DDataset, self).to_cxi(cxi_file)
|
||||
|
||||
# Now, we save out the polarizer and analyzer states
|
||||
cdtdata.add_shot_to_shot_info(cxi_file, self.polarizer,
|
||||
'polarizer_angle')
|
||||
cdtdata.add_shot_to_shot_info(cxi_file, self.analyzer,
|
||||
'analyzer_angle')
|
||||
|
||||
def inspect(self, logarithmic=True, units='um'):
|
||||
"""Launches an interactive plot for perusing the data
|
||||
|
||||
This launches an interactive plotting tool in matplotlib that
|
||||
shows the spatial map constructed from the integrated intensity
|
||||
at each position on the left, next to a panel on the right that
|
||||
can display a base-10 log plot of the detector readout at each
|
||||
position.
|
||||
"""
|
||||
|
||||
|
||||
def get_images(idx):
|
||||
inputs, output = self[idx]
|
||||
meas_data = output.detach().cpu().numpy()
|
||||
if hasattr(self, 'mask') and self.mask is not None:
|
||||
mask = self.mask.detach().cpu().numpy()
|
||||
else:
|
||||
mask = 1
|
||||
|
||||
if logarithmic:
|
||||
return np.log(meas_data) / np.log(10) * mask
|
||||
else:
|
||||
return meas_data * mask
|
||||
|
||||
translations = self.translations.detach().cpu().numpy()
|
||||
nanomap_values = (self.mask.to(t.float32) * self.patterns).sum(dim=(1,2)).detach().cpu().numpy()
|
||||
|
||||
if logarithmic:
|
||||
cbar_title='Log Base 10 of Diffraction Intensity'
|
||||
else:
|
||||
cbar_title='Diffraction Intensity'
|
||||
|
||||
plotting.plot_nanomap_with_images(self.translations.detach().cpu(), get_images, values=nanomap_values, nanomap_units=units, image_title='Diffraction Pattern', image_colorbar_title=cbar_title)
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
import numpy as np
|
||||
import torch as t
|
||||
from copy import copy
|
||||
import h5py
|
||||
import pathlib
|
||||
from CDTools.datasets import CDataset
|
||||
from CDTools.tools import data as cdtdata
|
||||
from CDTools.tools import plotting
|
||||
|
||||
__all__ = ['Ptycho2DDataset']
|
||||
|
||||
|
||||
class Ptycho2DDataset(CDataset):
|
||||
"""The standard dataset for a 2D ptychography scan
|
||||
|
||||
Subclasses datasets.CDataset
|
||||
|
||||
This class loads and saves 2D ptychography scan data from .cxi files.
|
||||
It should save and load files compatible with most reconstruction
|
||||
programs, although it is only tested against SHARP.
|
||||
"""
|
||||
def __init__(self, translations, patterns, intensities=None,
|
||||
axes=None, *args, **kwargs):
|
||||
"""The __init__ function allows construction from python objects.
|
||||
|
||||
The detector_geometry dictionary is defined to have the
|
||||
entries defined by the outputs of data.get_detector_geometry.
|
||||
|
||||
Note that the created dataset object will not copy the data in the
|
||||
patterns parameter in order to avoid doubling the memory requiement
|
||||
for large datasets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
translations : array
|
||||
An nx3 array containing the probe translations at each scan point
|
||||
patterns : array
|
||||
An nxmxl array containing the full stack of measured diffraction patterns
|
||||
axes : list(str)
|
||||
A list of names for the axes of the probe translations
|
||||
entry_info : dict
|
||||
A dictionary containing the entry_info metadata
|
||||
sample_info : dict
|
||||
A dictionary containing the sample_info metadata
|
||||
wavelength : float
|
||||
The wavelength of light used in the experiment
|
||||
detector_geometry : dict
|
||||
A dictionary containing the various detector geometry
|
||||
parameters
|
||||
mask : array
|
||||
A mask for the detector, defined as 1 for live pixels, 0
|
||||
for dead
|
||||
background : array
|
||||
An initial guess for the not-previously-subtracted
|
||||
detector background
|
||||
intensities : array
|
||||
A list of measured shot-to-shot intensities
|
||||
"""
|
||||
|
||||
|
||||
super(Ptycho2DDataset,self).__init__(*args, **kwargs)
|
||||
self.axes = copy(axes)
|
||||
self.translations = t.tensor(translations)
|
||||
|
||||
self.patterns = t.as_tensor(patterns)
|
||||
|
||||
if self.mask is None:
|
||||
self.mask = t.ones(self.patterns.shape[-2:]).to(dtype=t.bool)
|
||||
self.mask.masked_fill_(t.isnan(t.sum(self.patterns,dim=(0,))),0)
|
||||
self.patterns.masked_fill_(t.isnan(self.patterns),0)
|
||||
|
||||
if intensities is not None:
|
||||
self.intensities = t.as_tensor(intensities, dtype=t.float32)
|
||||
else:
|
||||
self.intensities = None
|
||||
|
||||
def __len__(self):
|
||||
return self.patterns.shape[0]
|
||||
|
||||
def _load(self, index):
|
||||
""" Internal function to load data
|
||||
|
||||
This function is used internally by the global __getitem__ function
|
||||
defined in the base class, which handles moving data around when
|
||||
the dataset is (for example) storing the data on the CPU but
|
||||
getting data as GPU tensors.
|
||||
|
||||
It loads data in the format (inputs, output)
|
||||
|
||||
The inputs for a 2D ptychogaphy data set are:
|
||||
|
||||
1) The indices of the patterns to use
|
||||
2) The recorded probe positions associated with those points
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : int or slice
|
||||
The index or indices of the scan points to use
|
||||
|
||||
Returns
|
||||
-------
|
||||
inputs : tuple
|
||||
A tuple of the inputs to the related forward models
|
||||
outputs : tuple
|
||||
The output pattern or stack of output patterns
|
||||
"""
|
||||
return (index, self.translations[index]), self.patterns[index]
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
"""Sends the relevant data to the given device and dtype
|
||||
|
||||
This function sends the stored translations, patterns,
|
||||
mask and background to the specified device and dtype
|
||||
|
||||
Accepts the same parameters as torch.Tensor.to
|
||||
"""
|
||||
super(Ptycho2DDataset,self).to(*args,**kwargs)
|
||||
self.translations = self.translations.to(*args, **kwargs)
|
||||
self.patterns = self.patterns.to(*args, **kwargs)
|
||||
|
||||
|
||||
# It sucks that I can't reuse the base factory method here,
|
||||
# perhaps there is a way but I couldn't figure it out.
|
||||
@classmethod
|
||||
def from_cxi(cls, cxi_file, cut_zeros=True, load_patterns=True):
|
||||
"""Generates a new Ptycho2DDataset from a .cxi file directly
|
||||
|
||||
This generates a new Ptycho2DDataset from a .cxi file storing
|
||||
a 2D ptychography scan.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to load from
|
||||
cut_zeros : bool
|
||||
Default True, whether to set all negative data to zero
|
||||
|
||||
Returns
|
||||
-------
|
||||
dataset : Ptycho2DDataset
|
||||
The constructed dataset object
|
||||
"""
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with h5py.File(cxi_file,'r') as f:
|
||||
return cls.from_cxi(f, cut_zeros=cut_zeros, load_patterns=load_patterns)
|
||||
|
||||
# Generate a base dataset
|
||||
dataset = CDataset.from_cxi(cxi_file)
|
||||
# Mutate the class to this subclass (BasicPtychoDataset)
|
||||
dataset.__class__ = cls
|
||||
|
||||
# Load the data that is only relevant for this class
|
||||
translations = cdtdata.get_ptycho_translations(cxi_file)
|
||||
# And now re-do the stuff from __init__
|
||||
dataset.translations = t.tensor(translations, dtype=t.float32)
|
||||
if load_patterns:
|
||||
patterns, axes = cdtdata.get_data(cxi_file, cut_zeros=cut_zeros)
|
||||
dataset.patterns = t.as_tensor(patterns)
|
||||
if dataset.patterns.dtype == t.float64:
|
||||
raise NotImplementedError('64-bit floats are not supported and precision will not be retained in reconstructions! Please explicitly convert your data to 32-bit or submit a pull request')
|
||||
|
||||
dataset.axes = axes
|
||||
|
||||
if dataset.mask is None:
|
||||
dataset.mask = t.ones(dataset.patterns.shape[-2:]).to(dtype=t.bool)
|
||||
|
||||
try:
|
||||
intensities = cdtdata.get_shot_to_shot_info(cxi_file, 'intensities')
|
||||
dataset.intensities = t.as_tensor(intensities, dtype=t.float32)
|
||||
except KeyError:
|
||||
dataset.intensities = None
|
||||
|
||||
return dataset
|
||||
|
||||
|
||||
def to_cxi(self, cxi_file):
|
||||
"""Saves out a Ptycho2DDataset as a .cxi file
|
||||
|
||||
This function saves all the compatible information in a
|
||||
Ptycho2DDataset object into a .cxi file. This saved .cxi file
|
||||
should be compatible with any standard .cxi file based
|
||||
reconstruction tool, such as SHARP.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : str, pathlib.Path, or h5py.File
|
||||
The .cxi file to write to
|
||||
"""
|
||||
|
||||
# If a bare string is passed
|
||||
if isinstance(cxi_file, str) or isinstance(cxi_file, pathlib.Path):
|
||||
with cdtdata.create_cxi(cxi_file) as f:
|
||||
return self.to_cxi(f)
|
||||
|
||||
super(Ptycho2DDataset,self).to_cxi(cxi_file)
|
||||
if hasattr(self, 'axes'):
|
||||
cdtdata.add_data(cxi_file, self.patterns, axes=self.axes)
|
||||
else:
|
||||
cdtdata.add_data(cxi_file, self.patterns)
|
||||
cdtdata.add_ptycho_translations(cxi_file, self.translations)
|
||||
|
||||
if hasattr(self, 'intensities') and self.intensities is not None:
|
||||
cdtdata.add_shot_to_shot_info(cxi_file, self.intensities, 'intensities')
|
||||
|
||||
|
||||
def inspect(self, logarithmic=True, units='um', log_offset=1):
|
||||
"""Launches an interactive plot for perusing the data
|
||||
|
||||
This launches an interactive plotting tool in matplotlib that
|
||||
shows the spatial map constructed from the integrated intensity
|
||||
at each position on the left, next to a panel on the right that
|
||||
can display a base-10 log plot of the detector readout at each
|
||||
position.
|
||||
"""
|
||||
|
||||
|
||||
def get_images(idx):
|
||||
inputs, output = self[idx]
|
||||
meas_data = output.detach().cpu().numpy()
|
||||
if hasattr(self, 'mask') and self.mask is not None:
|
||||
mask = self.mask.detach().cpu().numpy()
|
||||
else:
|
||||
mask = 1
|
||||
|
||||
if logarithmic:
|
||||
return np.log(meas_data + log_offset) / np.log(10) * mask
|
||||
else:
|
||||
return meas_data * mask
|
||||
|
||||
translations = self.translations.detach().cpu().numpy()
|
||||
|
||||
# This takes about twice as long as it would to just do it all at
|
||||
# once, but it avoids creating another self.patterns-sized array
|
||||
# as an intermediate step. This can be super important because
|
||||
# self.patterns can be more than half the available memory
|
||||
nanomap_values = np.ones(self.translations.shape[0])
|
||||
|
||||
chunk_size = 10
|
||||
for i in range(0, self.translations.shape[0], chunk_size):
|
||||
nanomap_values[i:i+chunk_size] = \
|
||||
t.sum(self.mask * self.patterns[i:i+chunk_size],dim=(1,2))
|
||||
|
||||
# This is the faster but more memory-intensive version
|
||||
# nanomap_values = (self.mask * self.patterns).sum(dim=(1,2)).detach().cpu().numpy()
|
||||
|
||||
if logarithmic:
|
||||
cbar_title = ('Log Base 10 of Diffraction Intensity + %0.2f'
|
||||
% log_offset)
|
||||
else:
|
||||
cbar_title = 'Diffraction Intensity'
|
||||
|
||||
return plotting.plot_nanomap_with_images(self.translations.detach().cpu(), get_images, values=nanomap_values, nanomap_units=units, image_title='Diffraction Pattern', image_colorbar_title=cbar_title)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""This module contains all the models for different CDI Reconstructions
|
||||
|
||||
All the reconstructions are coordinated through the ptychography models
|
||||
defined here. The models are, at their core, just subclasses of the
|
||||
:code:`torch.nn.model` class, so they contain the same structure of
|
||||
parameters, etc. Their central functionality is as a simulation that maps
|
||||
some input (usually, the index number of a scan point) to an output that
|
||||
corresponds to the measured data (usually, a diffraction pattern). This
|
||||
model can then be used as the heart of an automatic differentiation
|
||||
reconstruction which retrieves the parameters that were used in the model.
|
||||
|
||||
A main CDIModel class is defined in the base.py file, and models for
|
||||
various CDI geometries can be defined as subclasses of this base model.
|
||||
The subclasses of the main CDIModel class are required to implement a set of
|
||||
functions defined in the base.py file. Example implementations of
|
||||
these functions can be found in the code for the SimplePtycho class.
|
||||
|
||||
Finally, it is recommended to read through the tutorial section on
|
||||
defining a new ptychography model before attempting to do so.
|
||||
|
||||
"""
|
||||
|
||||
# I don't believe that __all__ really needed, but it's nice to define it
|
||||
# to be explicit that import * is safe
|
||||
__all__ = ['CDIModel', 'SimplePtycho', 'FancyPtycho', 'PolarizedFancyPtycho', 'Bragg2DPtycho', 'Multislice2DPtycho', 'RPI', 'TimeResolvedPtychoCalibration', 'TimeResolvedRPI']
|
||||
|
||||
from CDTools.models.base import CDIModel
|
||||
from CDTools.models.simple_ptycho import SimplePtycho
|
||||
from CDTools.models.fancy_ptycho import FancyPtycho
|
||||
from CDTools.models.polarized_fancy_ptycho import PolarizedFancyPtycho
|
||||
from CDTools.models.bragg_2d_ptycho import Bragg2DPtycho
|
||||
from CDTools.models.multislice_2d_ptycho import Multislice2DPtycho
|
||||
from CDTools.models.rpi import RPI
|
||||
from CDTools.models.multimode_rpi import MultimodeRPI
|
||||
from CDTools.models.time_resolved_ptycho_calibration import TimeResolvedPtychoCalibration
|
||||
from CDTools.models.time_resolved_rpi import TimeResolvedRPI
|
||||
|
||||
# Still needs to be updated for the new complex numbers
|
||||
#from CDTools.models.s_matrix_ptycho import SMatrixPtycho
|
||||
|
||||
@@ -0,0 +1,655 @@
|
||||
"""This module contains the base CDIModel class for CDI Models.
|
||||
|
||||
The subclasses of the main CDIModel class are required to define their
|
||||
own implementations of the following functions:
|
||||
|
||||
Loading and Saving
|
||||
------------------
|
||||
from_dataset
|
||||
Creates a CDIModel from an appropriate CDataset
|
||||
simulate_to_dataset
|
||||
Creates a CDataset from the simulation defined in the model
|
||||
save_results
|
||||
Saves out a dictionary with the recovered parameters
|
||||
|
||||
|
||||
Simulation
|
||||
----------
|
||||
interaction
|
||||
Simulates exit waves from experimental parameters
|
||||
forward_propagator
|
||||
The propagator from the experiment plane to the detector plane
|
||||
backward_propagator
|
||||
Optional, the propagator from the detector plane to the experiment plane
|
||||
measurement
|
||||
Simulates the detector readout from a detector plane wavefront
|
||||
loss
|
||||
the loss function to report and use for automatic differentiation
|
||||
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
from torch.utils import data as torchdata
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.widgets import Slider
|
||||
from matplotlib import ticker
|
||||
import numpy as np
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
from .complex_adam import MyAdam
|
||||
from .complex_lbfgs import MyLBFGS
|
||||
|
||||
__all__ = ['CDIModel']
|
||||
|
||||
|
||||
class CDIModel(t.nn.Module):
|
||||
"""This base model defines all the functions that must be exposed for a valid CDIModel subclass
|
||||
|
||||
Most of the functions only raise a NotImplementedError at this level and
|
||||
must be explicitly defined by any subclass - these are noted explocitly
|
||||
in the module-level intro. The work of defining the various subclasses
|
||||
boils down to creating an appropriate implementation for this set of
|
||||
functions.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CDIModel,self).__init__()
|
||||
|
||||
self.loss_train = []
|
||||
self.iteration_count = 0
|
||||
|
||||
def from_dataset(self, dataset):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def interaction(self, *args):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def forward_propagator(self, exit_wave):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def backward_propagator(self, detector_wave):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def measurement(self, detector_wave):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def forward(self, *args):
|
||||
"""The complete forward model
|
||||
|
||||
This model relies on composing the interaction, forward propagator,
|
||||
and measurement functions which are required to be defined by all
|
||||
subclasses. It therefore should not be redefined by the subclasses.
|
||||
|
||||
The arguments to this function, for any given subclass, will be
|
||||
the same as the arguments to the interaction function.
|
||||
"""
|
||||
return self.measurement(self.forward_propagator(self.interaction(*args)))
|
||||
|
||||
def loss(self, sim_data, real_data):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(CDIModel,self).to(*args,**kwargs)
|
||||
|
||||
|
||||
def simulate_to_dataset(self, args_list):
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_results(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
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.
|
||||
|
||||
Like all the other optimization routines, it is defined as a
|
||||
generator function which yields the average loss each epoch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterations : int
|
||||
How many epochs of the algorithm to run
|
||||
dataset : CDataset
|
||||
The dataset to reconstruct against
|
||||
optimizer : torch.optim.Optimizer
|
||||
The optimizer to run the reconstruction with
|
||||
scheduler : torch.optim.lr_scheduler._LRScheduler
|
||||
Optional, a learning rate scheduler to use
|
||||
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
|
||||
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()
|
||||
|
||||
def run_iteration(stop_event=None):
|
||||
loss = 0
|
||||
N = 0
|
||||
t0 = time.time()
|
||||
for inputs, patterns in data_loader:
|
||||
N += 1
|
||||
def closure():
|
||||
optimizer.zero_grad()
|
||||
|
||||
input_chunks = [[inp[i:i + calculation_width]
|
||||
for inp in inputs]
|
||||
for i in range(0, len(inputs[0]),
|
||||
calculation_width)]
|
||||
pattern_chunks = [patterns[i:i + calculation_width]
|
||||
for i in range(0, len(inputs[0]),
|
||||
calculation_width)]
|
||||
|
||||
total_loss = 0
|
||||
for inp, pats in zip(input_chunks, pattern_chunks):
|
||||
# This is just used to allow graceful exit when
|
||||
# threading
|
||||
if stop_event is not None and stop_event.is_set():
|
||||
exit()
|
||||
|
||||
sim_patterns = self.forward(*inp)
|
||||
|
||||
if hasattr(self, 'mask'):
|
||||
loss = self.loss(pats,sim_patterns, mask=self.mask)
|
||||
else:
|
||||
loss = self.loss(pats,sim_patterns)
|
||||
|
||||
loss.backward()
|
||||
|
||||
total_loss += loss.detach()
|
||||
|
||||
if regularization_factor is not None \
|
||||
and hasattr(self, 'regularizer'):
|
||||
loss = self.regularizer(regularization_factor)
|
||||
loss.backward()
|
||||
return total_loss
|
||||
|
||||
loss += optimizer.step(closure).detach().cpu().numpy()
|
||||
|
||||
loss /= normalization
|
||||
if scheduler is not None:
|
||||
scheduler.step(loss)
|
||||
|
||||
self.loss_train.append(loss)
|
||||
self.latest_iteration_time = time.time() - t0
|
||||
return loss
|
||||
|
||||
if thread:
|
||||
result_queue = queue.Queue()
|
||||
stop_event = threading.Event()
|
||||
def target():
|
||||
try:
|
||||
result_queue.put(run_iteration(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:
|
||||
calc = threading.Thread(target=target, name='calculator', daemon=True)
|
||||
try:
|
||||
calc.start()
|
||||
while calc.is_alive():
|
||||
if hasattr(self, 'figs'):
|
||||
self.figs[0].canvas.start_event_loop(0.01)
|
||||
else:
|
||||
calc.join()
|
||||
|
||||
except KeyboardInterrupt as e:
|
||||
stop_event.set()
|
||||
print('\nAsking execution thread to stop cleanly - please be patient.')
|
||||
calc.join()
|
||||
raise e
|
||||
|
||||
res = result_queue.get()
|
||||
|
||||
# If something went wrong in the thead, we'll get an exception
|
||||
if isinstance(res, Exception):
|
||||
raise res
|
||||
|
||||
yield res
|
||||
|
||||
else:
|
||||
yield run_iteration()
|
||||
|
||||
|
||||
def Adam_optimize(self, iterations, dataset, batch_size=15, lr=0.005,
|
||||
schedule=False, amsgrad=False, subset=None,
|
||||
regularization_factor=None, thread=True,
|
||||
calculation_width=10):
|
||||
"""Runs a round of reconstruction using the Adam optimizer
|
||||
|
||||
This is generally accepted to be the most robust algorithm for use
|
||||
with ptychography. Like all the other optimization routines,
|
||||
it is defined as a generator function, which yields the average
|
||||
loss each epoch.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterations : int
|
||||
How many epochs of the algorithm to run
|
||||
dataset : CDataset
|
||||
The dataset to reconstruct against
|
||||
batch_size : int
|
||||
Optional, the size of the minibatches to use
|
||||
lr : float
|
||||
Optional, The learning rate (alpha) to use
|
||||
schedule : float
|
||||
Optional, whether to use the ReduceLROnPlateau scheduler
|
||||
subset : list(int) or int
|
||||
Optional, a pattern index or list of pattern indices to use
|
||||
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
|
||||
calculation_width : int
|
||||
Default 1, how many translations to pass through at once for each round of gradient accumulation
|
||||
|
||||
"""
|
||||
|
||||
if subset is not None:
|
||||
# if 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,
|
||||
shuffle=True)
|
||||
|
||||
# Define the optimizer
|
||||
#optimizer = t.optim.Adam(self.parameters(), lr = lr, amsgrad=amsgrad)
|
||||
optimizer = MyAdam(self.parameters(), lr = lr, amsgrad=amsgrad)
|
||||
|
||||
|
||||
# Define the scheduler
|
||||
if schedule:
|
||||
scheduler = t.optim.lr_scheduler.ReduceLROnPlateau(optimizer, factor=0.2,threshold=1e-9)
|
||||
else:
|
||||
scheduler = None
|
||||
|
||||
return self.AD_optimize(iterations, data_loader, optimizer,
|
||||
scheduler=scheduler,
|
||||
regularization_factor=regularization_factor,
|
||||
thread=thread,
|
||||
calculation_width=calculation_width)
|
||||
|
||||
|
||||
def LBFGS_optimize(self, iterations, dataset,
|
||||
lr=0.1,history_size=2, subset=None,
|
||||
regularization_factor=None, thread=True,
|
||||
calculation_width=10, line_search_fn=None):
|
||||
"""Runs a round of reconstruction using the L-BFGS optimizer
|
||||
|
||||
This algorithm is often less stable that Adam, however in certain
|
||||
situations or geometries it can be shockingly efficient. Like all
|
||||
the other optimization routines, it is defined as a generator
|
||||
function which yields the average loss each epoch.
|
||||
|
||||
Note: There is no batch size, because it is a usually a bad idea to use
|
||||
LBFGS on anything but all the data at onece
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterations : int
|
||||
How many epochs of the algorithm to run
|
||||
dataset : CDataset
|
||||
The dataset to reconstruct against
|
||||
lr : float
|
||||
Optional, the learning rate to use
|
||||
history_size : int
|
||||
Optional, the length of the history to use.
|
||||
subset : list(int) or int
|
||||
Optional, a pattern index or list of pattern indices to ues
|
||||
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
|
||||
|
||||
"""
|
||||
if subset is not None:
|
||||
# if just one pattern, turn into a list for convenience
|
||||
if type(subset) == type(1):
|
||||
subset = [subset]
|
||||
dataset = torchdata.Subset(dataset, subset)
|
||||
|
||||
# Make a dataloader. This basically does nothing but load all the
|
||||
# data at once
|
||||
data_loader = torchdata.DataLoader(dataset, batch_size=len(dataset))
|
||||
|
||||
|
||||
# Define the optimizer
|
||||
optimizer = t.optim.LBFGS(self.parameters(),
|
||||
lr = lr, history_size=history_size,
|
||||
line_search_fn=line_search_fn)
|
||||
#optimizer = MyLBFGS(self.parameters(),
|
||||
# lr = lr, history_size=history_size)
|
||||
|
||||
return self.AD_optimize(iterations, data_loader, optimizer,
|
||||
regularization_factor=regularization_factor,
|
||||
thread=thread,
|
||||
calculation_width=calculation_width)
|
||||
|
||||
|
||||
def SGD_optimize(self, iterations, dataset, batch_size=None,
|
||||
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
|
||||
|
||||
This algorithm is often less stable that Adam, but it is simpler
|
||||
and is the basic workhorse of gradience descent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterations : int
|
||||
How many epochs of the algorithm to run
|
||||
dataset : CDataset
|
||||
The dataset to reconstruct against
|
||||
batch_size : int
|
||||
Optional, the size of the minibatches to use
|
||||
lr : float
|
||||
Optional, the learning rate to use
|
||||
momentum : float
|
||||
Optional, the length of the history to use.
|
||||
subset : list(int) or int
|
||||
Optional, a pattern index or list of pattern indices to use
|
||||
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
|
||||
calculation_width : int
|
||||
Default 1, how many translations to pass through at once for each round of gradient accumulation
|
||||
|
||||
"""
|
||||
|
||||
if subset is not None:
|
||||
# if just one pattern, turn into a list for convenience
|
||||
if type(subset) == type(1):
|
||||
subset = [subset]
|
||||
dataset = torchdata.Subset(dataset, subset)
|
||||
|
||||
# Make a dataloader
|
||||
if batch_size is not None:
|
||||
data_loader = torchdata.DataLoader(dataset, batch_size=batch_size,
|
||||
shuffle=True)
|
||||
else:
|
||||
data_loader = torchdata.DataLoader(dataset)
|
||||
|
||||
|
||||
# Define the optimizer
|
||||
optimizer = t.optim.SGD(self.parameters(),
|
||||
lr = lr, momentum=momentum,
|
||||
dampening=dampening,
|
||||
weight_decay=weight_decay,
|
||||
nesterov=nesterov)
|
||||
|
||||
return self.AD_optimize(iterations, data_loader, optimizer,
|
||||
regularization_factor=regularization_factor,
|
||||
thread=thread,
|
||||
calculation_width=calculation_width)
|
||||
|
||||
|
||||
def report(self):
|
||||
"""Returns a string informing on the latest reconstruction iteration
|
||||
|
||||
Returns
|
||||
-------
|
||||
report : str
|
||||
A string with basic info on the latest iteration
|
||||
"""
|
||||
if hasattr(self, 'latest_iteration_time'):
|
||||
return 'Iteration ' + str(len(self.loss_train)) + \
|
||||
' completed in %0.2f s with loss ' %\
|
||||
self.latest_iteration_time + str(self.loss_train[-1])
|
||||
else:
|
||||
return 'No reconstruction iterations performed yet!'
|
||||
|
||||
# By default, the plot_list is empty
|
||||
plot_list = []
|
||||
|
||||
|
||||
def inspect(self, dataset=None, update=True):
|
||||
"""Plots all the plots defined in the model's plot_list attribute
|
||||
|
||||
If update is set to True, it will update any previously plotted set
|
||||
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
|
||||
registered plots which need to incorporate some information from
|
||||
the dataset (such as geometry or a comparison with measured data).
|
||||
|
||||
Plots can be registered in any subclass by defining the plot_list
|
||||
attribute. This should be a list of tuples in the following format:
|
||||
( 'Plot Title', function_to_generate_plot(self),
|
||||
function_to_determine_whether_to_plot(self))
|
||||
|
||||
Where the third element in the tuple (a function that returns
|
||||
True if the plot is relevant) is not required.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : CDataset
|
||||
Optional, a dataset matched to the model type
|
||||
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])
|
||||
|
||||
first_update = False
|
||||
if update and hasattr(self, 'figs') and self.figs:
|
||||
figs = self.figs
|
||||
elif update:
|
||||
figs = None
|
||||
self.figs = []
|
||||
first_update = True
|
||||
else:
|
||||
figs = None
|
||||
self.figs = []
|
||||
|
||||
idx = 0
|
||||
for plots in self.plot_list:
|
||||
# If a conditional is included in the plot
|
||||
try:
|
||||
if len(plots) >=3 and not plots[2](self):
|
||||
continue
|
||||
except TypeError as e:
|
||||
if len(plots) >= 3 and not plots[2](self, dataset):
|
||||
continue
|
||||
|
||||
name = plots[0]
|
||||
plotter = plots[1]
|
||||
|
||||
if figs is None:
|
||||
fig = plt.figure()
|
||||
self.figs.append(fig)
|
||||
else:
|
||||
fig = figs[idx]
|
||||
|
||||
try:
|
||||
plotter(self,fig)
|
||||
plt.title(name)
|
||||
|
||||
except TypeError as e:
|
||||
if dataset is not None:
|
||||
try:
|
||||
plotter(self, fig, dataset)
|
||||
plt.title(name)
|
||||
|
||||
except (IndexError, KeyError, AttributeError, np.linalg.LinAlgError) as e:
|
||||
pass
|
||||
|
||||
except (IndexError, KeyError, AttributeError, np.linalg.LinAlgError) as e:
|
||||
pass
|
||||
|
||||
idx += 1
|
||||
|
||||
if update:
|
||||
plt.draw()
|
||||
fig.canvas.start_event_loop(0.001)
|
||||
|
||||
if first_update:
|
||||
plt.pause(0.05 * len(self.figs))
|
||||
|
||||
|
||||
def save_figures(self, prefix='', extension='.eps'):
|
||||
"""Saves all currently open inspection figures.
|
||||
|
||||
Note that this function is not very intelligent - so, for example,
|
||||
if multiple probe modes are being reconstructed and the probe
|
||||
plotting function allows one to scroll between different modes, it
|
||||
will simply save whichever mode happens to be showing at the moment.
|
||||
Therefore, this should not be treated as a good way of saving out
|
||||
the full state of the reconstruction.
|
||||
|
||||
By default, the files will be named by the figure titles as defined
|
||||
in the plot_list. Files can be saved with any extension suported by
|
||||
matplotlib.pyplot.savefig.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prefix : str
|
||||
Optional, a string to prepend to the saved figure names
|
||||
extention : strategy
|
||||
Default is .eps, the file extension to save with.
|
||||
"""
|
||||
|
||||
if hasattr(self, 'figs') and self.figs:
|
||||
figs = self.figs
|
||||
else:
|
||||
return # No figures to save
|
||||
|
||||
for fig in self.figs:
|
||||
fig.savefig(prefix + fig.axes[0].get_title() + extension,
|
||||
bbox_inches = 'tight')
|
||||
|
||||
|
||||
def compare(self, dataset, logarithmic=False):
|
||||
"""Opens a tool for comparing simulated and measured diffraction patterns
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : CDataset
|
||||
A dataset containing the simulated diffraction patterns to compare against
|
||||
"""
|
||||
|
||||
fig, axes = plt.subplots(1,3,figsize=(12,5.3))
|
||||
fig.tight_layout(rect=[0.02, 0.09, 0.98, 0.96])
|
||||
axslider = plt.axes([0.15,0.06,0.75,0.03])
|
||||
|
||||
|
||||
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
|
||||
|
||||
im.norecurse=True
|
||||
im.set_clim(vmin=np.min(im.get_array()),vmax=np.max(im.get_array()))
|
||||
|
||||
def update(idx):
|
||||
idx = int(idx) % len(dataset)
|
||||
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()
|
||||
if hasattr(self, 'mask') and self.mask is not None:
|
||||
mask = self.mask.detach().cpu().numpy()
|
||||
else:
|
||||
mask = 1
|
||||
|
||||
if logarithmic:
|
||||
sim_data =np.log(sim_data)/np.log(10)
|
||||
meas_data = np.log(meas_data)/np.log(10)
|
||||
|
||||
if not updating:
|
||||
axes[0].set_title('Simulated')
|
||||
axes[1].set_title('Measured')
|
||||
axes[2].set_title('Difference')
|
||||
|
||||
sim = axes[0].imshow(sim_data)
|
||||
meas = axes[1].imshow(meas_data * mask)
|
||||
diff = axes[2].imshow((sim_data-meas_data) * mask)
|
||||
|
||||
cb1 = plt.colorbar(sim, ax=axes[0], orientation='horizontal',format='%.2e',ticks=ticker.LinearLocator(numticks=5),pad=0.1,fraction=0.1)
|
||||
cb1.ax.tick_params(labelrotation=20)
|
||||
cb1.ax.callbacks.connect('xlim_changed', lambda ax: update_colorbar(sim))
|
||||
cb2 = plt.colorbar(meas, ax=axes[1], orientation='horizontal',format='%.2e',ticks=ticker.LinearLocator(numticks=5),pad=0.1,fraction=0.1)
|
||||
cb2.ax.tick_params(labelrotation=20)
|
||||
cb2.ax.callbacks.connect('xlim_changed', lambda ax: update_colorbar(meas))
|
||||
cb3 = plt.colorbar(diff, ax=axes[2], orientation='horizontal',format='%.2e',ticks=ticker.LinearLocator(numticks=5),pad=0.1,fraction=0.1)
|
||||
cb3.ax.tick_params(labelrotation=20)
|
||||
cb3.ax.callbacks.connect('xlim_changed', lambda ax: update_colorbar(diff))
|
||||
|
||||
else:
|
||||
|
||||
sim = axes[0].images[-1]
|
||||
sim.set_data(sim_data)
|
||||
update_colorbar(sim)
|
||||
|
||||
meas = axes[1].images[-1]
|
||||
meas.set_data(meas_data * mask)
|
||||
update_colorbar(meas)
|
||||
|
||||
diff = axes[2].images[-1]
|
||||
diff.set_data((sim_data-meas_data) * mask)
|
||||
update_colorbar(diff)
|
||||
|
||||
|
||||
# This is dumb but the slider doesn't work unless a reference to it is
|
||||
# kept somewhere...
|
||||
self.slider = Slider(axslider, 'Pattern #', 0, len(dataset)-1, valstep=1, valfmt="%d")
|
||||
self.slider.on_changed(update)
|
||||
|
||||
def on_action(event):
|
||||
if not hasattr(event, 'button'):
|
||||
event.button = None
|
||||
if not hasattr(event, 'key'):
|
||||
event.key = None
|
||||
|
||||
if event.key == 'up' or event.button == 'up':
|
||||
update(fig.pattern_idx - 1)
|
||||
elif event.key == 'down' or event.button == 'down':
|
||||
update(fig.pattern_idx + 1)
|
||||
self.slider.set_val(fig.pattern_idx)
|
||||
plt.draw()
|
||||
|
||||
fig.canvas.mpl_connect('key_press_event',on_action)
|
||||
fig.canvas.mpl_connect('scroll_event',on_action)
|
||||
update(0)
|
||||
@@ -0,0 +1,533 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools.propagators import generate_generalized_angular_spectrum_propagator as ggasp
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
|
||||
__all__ = ['Bragg2DPtycho']
|
||||
|
||||
#
|
||||
# Key ideas:
|
||||
# 1) To a first approximation, do the reconstruction on a parallelogram
|
||||
# shaped grid on the sample which is conjugate to the detector coordinates
|
||||
# 2) Simulate a probe in those same coordinates, but propagate it back and
|
||||
# forth (along with the translations), using the angular spectrum method
|
||||
# 3) Apply a correction to the simulated data to account for the tilt of the
|
||||
# sample with respect to the detector
|
||||
# 4) Include a correction for the thickness of the sample
|
||||
#
|
||||
|
||||
#
|
||||
# How to do this properly?
|
||||
# First thing to note is that the two corrections (probe propagation before
|
||||
# interaction and high-NA correction for the final diffraction measurement)
|
||||
# should be able to be turned on separately, since they show up in different
|
||||
# situations. In fact, I would like to focus on the first aspect initially
|
||||
# since I think that's the dominant issue we will contend with at CSX.
|
||||
#
|
||||
|
||||
#
|
||||
# It also should be possible to choose an "auto" setting for the two
|
||||
# corrections, since the geometry information given should be enough to
|
||||
# decide if the correction is needed.
|
||||
# Probably the automatic check will have to be very conservative for the
|
||||
# probe propagation side since the model has no information about the
|
||||
# expected numerical aperture of the probe.
|
||||
#
|
||||
|
||||
#
|
||||
# I'm worried that the propagation doesn't happen along the correct
|
||||
# direction, if a phase ramp is expected to be baked in to the
|
||||
# retrieved focal spot. Unclear if this is the case though.
|
||||
#
|
||||
# Retrieved focal spot should have the implicit phase ramp subtracted
|
||||
# (that is, it should be the focal spot along the sample plane, but with
|
||||
# the e^ikz dependence removed). Therefore, the original e^ikz dependence
|
||||
# should be easy to re-add jusy by using the propagate_along feature
|
||||
# in ggasp. So I believe this should not be a problem
|
||||
#
|
||||
|
||||
class Bragg2DPtycho(CDIModel):
|
||||
|
||||
# Needed to do the real/complex split
|
||||
#@property
|
||||
#def obj(self):
|
||||
# return t.complex(self.obj_real, self.obj_imag)
|
||||
|
||||
#@property
|
||||
#def probe(self):
|
||||
# return t.complex(self.probe_real, self.probe_imag)
|
||||
|
||||
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis, probe_guess, obj_guess,
|
||||
detector_slice=None,
|
||||
min_translation=t.tensor([0, 0], dtype=t.float32),
|
||||
median_propagation=t.tensor(0, dtype=t.float32),
|
||||
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):
|
||||
|
||||
# We need the detector geometry
|
||||
# We need the probe basis (but in this case, we don't need the surface
|
||||
# normal because it comes implied by the probe basis
|
||||
# we do need the detector slice I suppose
|
||||
# The min translation is also needed
|
||||
# The median propagation should be needed as well
|
||||
# translation_offsets can stay 2D for now
|
||||
# propagate_probe and correct_tilt are important!
|
||||
|
||||
super(Bragg2DPtycho, self).__init__()
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.min_translation = t.tensor(min_translation)
|
||||
self.median_propagation = t.tensor(median_propagation)
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.detector_slice = copy(detector_slice)
|
||||
|
||||
# calculate the surface normal from the probe basis
|
||||
surface_normal = np.cross(np.array(probe_basis)[:,1],
|
||||
np.array(probe_basis)[:,0])
|
||||
surface_normal /= np.linalg.norm(surface_normal)
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.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)
|
||||
|
||||
# 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]))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
|
||||
# Not strictly necessary but otherwise it will return
|
||||
# a probe with the stuff outside of the support unchanged after
|
||||
# optimization
|
||||
if probe_support is not None:
|
||||
self.probe_support = t.tensor(probe_support, dtype=t.bool)
|
||||
probe_guess = probe_guess * probe_support
|
||||
# This seems dumb, but otherwise it winds up with a mixture
|
||||
# of negative and positive zeros and it's super annoying when
|
||||
# you look at the phase map
|
||||
probe_guess[probe_guess == 0] = 0
|
||||
else:
|
||||
self.probe_support = t.ones(probe_guess[0].shape, dtype=t.bool)
|
||||
|
||||
#self.probe_real = t.nn.Parameter(probe_guess.real / self.probe_norm)
|
||||
#self.probe_imag = t.nn.Parameter(probe_guess.imag / self.probe_norm)
|
||||
|
||||
#self.obj_real = t.nn.Parameter(obj_guess.real )
|
||||
#self.obj_imag = t.nn.Parameter(obj_guess.imag)
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[0][self.detector_slice].shape,
|
||||
dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.nn.Parameter(background)
|
||||
|
||||
if weights is None:
|
||||
self.weights = None
|
||||
else:
|
||||
# No incoherent + unstable here yet
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.float32))
|
||||
|
||||
if translation_offsets is None:
|
||||
self.translation_offsets = None
|
||||
else:
|
||||
t_o = t.tensor(translation_offsets, dtype=t.float32)
|
||||
t_o = t_o / translation_scale
|
||||
self.translation_offsets = t.nn.Parameter(t_o)
|
||||
|
||||
self.translation_scale = translation_scale
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
self.propagate_probe = propagate_probe
|
||||
self.correct_tilt = correct_tilt
|
||||
if correct_tilt:
|
||||
# recall that here we always want the shape of the detector
|
||||
# before it's cut down by the detector slice to match the
|
||||
# physical detector region
|
||||
probe_shape = self.probe[0].shape
|
||||
|
||||
self.k_map, self.intensity_map = \
|
||||
tools.propagators.generate_high_NA_k_intensity_map(
|
||||
self.probe_basis,
|
||||
self.detector_geometry['basis'] / oversampling,
|
||||
probe_shape,
|
||||
self.detector_geometry['distance'],
|
||||
self.wavelength,dtype=t.float32,
|
||||
lens=lens)
|
||||
else:
|
||||
self.k_map = None
|
||||
self.intensity_map = None
|
||||
|
||||
self.prop_dir = t.tensor([0, 0, 1], dtype=t.float32)
|
||||
|
||||
# This propagator should be able to be multiplied by the propagation
|
||||
# distance each time to get a propagator
|
||||
self.universal_propagator = t.angle(ggasp(
|
||||
self.probe.shape[1:],
|
||||
self.probe_basis, self.wavelength,
|
||||
t.tensor([0, 0, self.wavelength/(2*np.pi)], dtype=t.float32),
|
||||
propagation_vector=self.prop_dir,
|
||||
dtype=t.complex64,
|
||||
propagate_along_offset=True))
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe_size=None, randomize_ang=0, padding=0, n_modes=1, translation_scale = 1, saturation=None, probe_support_radius=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=True, propagate_probe=True, correct_tilt=True, lens=False, opt_for_fft=False):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
(indices, translations), patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns,dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the exit wave geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
ew_basis, ew_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
# now we grab the sample surface normal
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:,0], det_basis[:,1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0.,0.,1.])
|
||||
surface_normal /= np.linalg.norm(outgoing_dir)
|
||||
|
||||
# and we use that to generate the probe basis
|
||||
|
||||
ew_normal = np.cross(np.array(ew_basis)[:,1],
|
||||
np.array(ew_basis)[:,0])
|
||||
ew_normal /= np.linalg.norm(ew_normal)
|
||||
|
||||
# This is a bit of an odd way to do a projection but I think it's
|
||||
# the most compact and reliable. We set up two matrix-vector equations
|
||||
# to enforce the two conditions (on the plane normal to the surface
|
||||
# normal and in the ew_normal direction from the original point
|
||||
|
||||
mat = np.vstack([np.eye(3) - np.outer(ew_normal,ew_normal),
|
||||
surface_normal])
|
||||
|
||||
# We know that this matrix multiplied by the final result must
|
||||
# equal the input vector with a trailing 0, so we can do the
|
||||
# projection with a pseudoinverse and removing the last column
|
||||
|
||||
projector = np.linalg.pinv(mat)[:, :3]
|
||||
probe_basis = t.Tensor(np.dot(projector, ew_basis))
|
||||
|
||||
# Now we need a much better way to handle the translations here
|
||||
# than translations_to_pixel
|
||||
|
||||
# Next generate the object geometry from the probe geometry and
|
||||
# the translations
|
||||
p2s = tools.interactions.project_translations_to_sample
|
||||
pix_translations, propagations = p2s(probe_basis, translations)
|
||||
|
||||
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(ew_shape, pix_translations, padding=200)
|
||||
|
||||
median_propagation = t.median(propagations)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
# Because the grid we defined on the sample is projected from the
|
||||
# detector conjugate space, we can pretend that the grid is just in
|
||||
# that space and use the standard initializations anyway
|
||||
|
||||
if probe_size is None:
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, ew_shape, det_slice, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
else:
|
||||
probe = tools.initializers.gaussian_probe(dataset, ew_basis, ew_shape, probe_size, propagation_distance=propagation_distance)
|
||||
|
||||
|
||||
# Now we initialize all the subdominant probe modes
|
||||
probe_max = t.max(t.abs(probe))
|
||||
probe_stack = [0.01 * probe_max * t.rand(probe.shape,dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
probe = t.stack([probe,] + probe_stack)
|
||||
|
||||
obj = t.exp(1j*(randomize_ang * (t.rand(obj_size)-0.5)))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset),2)) - 0.5)
|
||||
|
||||
weights = t.ones(len(dataset))
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
if probe_support_radius is not None:
|
||||
probe_support = t.zeros(probe[0].shape, dtype=t.bool)
|
||||
xs, ys = np.mgrid[:probe.shape[-2], :probe.shape[-1]]
|
||||
xs = xs - np.mean(xs)
|
||||
ys = ys - np.mean(ys)
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
probe_support[Rs < probe_support_radius] = 1
|
||||
probe = probe * probe_support[None, :, :]
|
||||
|
||||
else:
|
||||
probe_support = None
|
||||
|
||||
# Here we need to implement a simple condition to choose whether
|
||||
# to propagate the probe or not
|
||||
if not( propagate_probe is True or propagate_probe is False):
|
||||
raise NotImplementedError('No auto option implemented yet')
|
||||
|
||||
if not(correct_tilt is True or correct_tilt is False):
|
||||
raise NotImplementedError('No auto option implemented yet')
|
||||
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, probe, obj,
|
||||
detector_slice=det_slice,
|
||||
min_translation=min_translation,
|
||||
median_propagation =median_propagation,
|
||||
translation_offsets = translation_offsets,
|
||||
weights=weights, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
probe_support=probe_support,
|
||||
oversampling=oversampling,
|
||||
propagate_probe=propagate_probe,
|
||||
correct_tilt=correct_tilt,
|
||||
lens=lens)
|
||||
|
||||
|
||||
def interaction(self, index, translations):
|
||||
pix_trans, props = tools.interactions.project_translations_to_sample(
|
||||
self.probe_basis, translations)
|
||||
|
||||
|
||||
pix_trans -= self.min_translation
|
||||
props -= self.median_propagation
|
||||
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += self.translation_scale * self.translation_offsets[index]
|
||||
|
||||
Ws = self.weights[index]
|
||||
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)
|
||||
|
||||
exit_waves = self.probe_norm * tools.interactions.ptycho_2D_sinc(
|
||||
prs, self.obj,pix_trans,
|
||||
shift_probe=True, multiple_modes=True)
|
||||
|
||||
return exit_waves
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
if self.correct_tilt:
|
||||
return tools.propagators.high_NA_far_field(
|
||||
wavefields,self.k_map,intensity_map=self.intensity_map)
|
||||
else:
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
if self.correct_tilt:
|
||||
assert NotImplementedError('Backward propagator not defined with tilt correction')
|
||||
else:
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(Bragg2DPtycho, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
|
||||
if self.k_map is not None:
|
||||
self.k_map = self.k_map.to(*args,**kwargs)
|
||||
if self.intensity_map is not None:
|
||||
self.intensity_map = self.intensity_map.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)
|
||||
self.prop_dir = self.prop_dir.to(*args, **kwargs)
|
||||
self.universal_propagator = self.universal_propagator.to(*args,**kwargs)
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def corrected_translations(self,dataset):
|
||||
translations = dataset.translations.to(dtype=self.probe.real.dtype,
|
||||
device=self.probe.device)
|
||||
t_offset = tools.interactions.pixel_to_translations(self.probe_basis,self.translation_offsets*self.translation_scale,surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
|
||||
|
||||
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),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig)),
|
||||
('Object Phase',
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
losses = np.array(self.loss_train)
|
||||
|
||||
return {'basis':basis, 'translation':translations,
|
||||
'probe':probe,'obj':obj,
|
||||
'background':background,
|
||||
'weights':weights,
|
||||
'losses':losses}
|
||||
@@ -0,0 +1,163 @@
|
||||
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)
|
||||
@@ -0,0 +1,485 @@
|
||||
import torch
|
||||
from functools import reduce
|
||||
from torch.optim.optimizer import Optimizer
|
||||
|
||||
|
||||
def _cubic_interpolate(x1, f1, g1, x2, f2, g2, bounds=None):
|
||||
# ported from https://github.com/torch/optim/blob/master/polyinterp.lua
|
||||
# Compute bounds of interpolation area
|
||||
if bounds is not None:
|
||||
xmin_bound, xmax_bound = bounds
|
||||
else:
|
||||
xmin_bound, xmax_bound = (x1, x2) if x1 <= x2 else (x2, x1)
|
||||
|
||||
# Code for most common case: cubic interpolation of 2 points
|
||||
# w/ function and derivative values for both
|
||||
# Solution in this case (where x2 is the farthest point):
|
||||
# d1 = g1 + g2 - 3*(f1-f2)/(x1-x2);
|
||||
# d2 = sqrt(d1^2 - g1*g2);
|
||||
# min_pos = x2 - (x2 - x1)*((g2 + d2 - d1)/(g2 - g1 + 2*d2));
|
||||
# t_new = min(max(min_pos,xmin_bound),xmax_bound);
|
||||
d1 = g1 + g2 - 3 * (f1 - f2) / (x1 - x2)
|
||||
d2_square = d1**2 - g1 * g2
|
||||
if d2_square >= 0:
|
||||
d2 = d2_square.sqrt()
|
||||
if x1 <= x2:
|
||||
min_pos = x2 - (x2 - x1) * ((g2 + d2 - d1) / (g2 - g1 + 2 * d2))
|
||||
else:
|
||||
min_pos = x1 - (x1 - x2) * ((g1 + d2 - d1) / (g1 - g2 + 2 * d2))
|
||||
return min(max(min_pos, xmin_bound), xmax_bound)
|
||||
else:
|
||||
return (xmin_bound + xmax_bound) / 2.
|
||||
|
||||
|
||||
def _strong_wolfe(obj_func,
|
||||
x,
|
||||
t,
|
||||
d,
|
||||
f,
|
||||
g,
|
||||
gtd,
|
||||
c1=1e-4,
|
||||
c2=0.9,
|
||||
tolerance_change=1e-9,
|
||||
max_ls=25):
|
||||
# ported from https://github.com/torch/optim/blob/master/lswolfe.lua
|
||||
d_norm = d.abs().max()
|
||||
g = g.clone(memory_format=torch.contiguous_format)
|
||||
# evaluate objective and gradient using initial step
|
||||
f_new, g_new = obj_func(x, t, d)
|
||||
ls_func_evals = 1
|
||||
gtd_new = g_new.dot(d)
|
||||
|
||||
# bracket an interval containing a point satisfying the Wolfe criteria
|
||||
t_prev, f_prev, g_prev, gtd_prev = 0, f, g, gtd
|
||||
done = False
|
||||
ls_iter = 0
|
||||
while ls_iter < max_ls:
|
||||
# check conditions
|
||||
if f_new > (f + c1 * t * gtd) or (ls_iter > 1 and f_new >= f_prev):
|
||||
bracket = [t_prev, t]
|
||||
bracket_f = [f_prev, f_new]
|
||||
bracket_g = [g_prev, g_new.clone(memory_format=torch.contiguous_format)]
|
||||
bracket_gtd = [gtd_prev, gtd_new]
|
||||
break
|
||||
|
||||
if abs(gtd_new) <= -c2 * gtd:
|
||||
bracket = [t]
|
||||
bracket_f = [f_new]
|
||||
bracket_g = [g_new]
|
||||
done = True
|
||||
break
|
||||
|
||||
if gtd_new >= 0:
|
||||
bracket = [t_prev, t]
|
||||
bracket_f = [f_prev, f_new]
|
||||
bracket_g = [g_prev, g_new.clone(memory_format=torch.contiguous_format)]
|
||||
bracket_gtd = [gtd_prev, gtd_new]
|
||||
break
|
||||
|
||||
# interpolate
|
||||
min_step = t + 0.01 * (t - t_prev)
|
||||
max_step = t * 10
|
||||
tmp = t
|
||||
t = _cubic_interpolate(
|
||||
t_prev,
|
||||
f_prev,
|
||||
gtd_prev,
|
||||
t,
|
||||
f_new,
|
||||
gtd_new,
|
||||
bounds=(min_step, max_step))
|
||||
|
||||
# next step
|
||||
t_prev = tmp
|
||||
f_prev = f_new
|
||||
g_prev = g_new.clone(memory_format=torch.contiguous_format)
|
||||
gtd_prev = gtd_new
|
||||
f_new, g_new = obj_func(x, t, d)
|
||||
ls_func_evals += 1
|
||||
gtd_new = g_new.dot(d)
|
||||
ls_iter += 1
|
||||
|
||||
# reached max number of iterations?
|
||||
if ls_iter == max_ls:
|
||||
bracket = [0, t]
|
||||
bracket_f = [f, f_new]
|
||||
bracket_g = [g, g_new]
|
||||
|
||||
# zoom phase: we now have a point satisfying the criteria, or
|
||||
# a bracket around it. We refine the bracket until we find the
|
||||
# exact point satisfying the criteria
|
||||
insuf_progress = False
|
||||
# find high and low points in bracket
|
||||
low_pos, high_pos = (0, 1) if bracket_f[0] <= bracket_f[-1] else (1, 0)
|
||||
while not done and ls_iter < max_ls:
|
||||
# line-search bracket is so small
|
||||
if abs(bracket[1] - bracket[0]) * d_norm < tolerance_change:
|
||||
break
|
||||
|
||||
# compute new trial value
|
||||
t = _cubic_interpolate(bracket[0], bracket_f[0], bracket_gtd[0],
|
||||
bracket[1], bracket_f[1], bracket_gtd[1])
|
||||
|
||||
# test that we are making sufficient progress:
|
||||
# in case `t` is so close to boundary, we mark that we are making
|
||||
# insufficient progress, and if
|
||||
# + we have made insufficient progress in the last step, or
|
||||
# + `t` is at one of the boundary,
|
||||
# we will move `t` to a position which is `0.1 * len(bracket)`
|
||||
# away from the nearest boundary point.
|
||||
eps = 0.1 * (max(bracket) - min(bracket))
|
||||
if min(max(bracket) - t, t - min(bracket)) < eps:
|
||||
# interpolation close to boundary
|
||||
if insuf_progress or t >= max(bracket) or t <= min(bracket):
|
||||
# evaluate at 0.1 away from boundary
|
||||
if abs(t - max(bracket)) < abs(t - min(bracket)):
|
||||
t = max(bracket) - eps
|
||||
else:
|
||||
t = min(bracket) + eps
|
||||
insuf_progress = False
|
||||
else:
|
||||
insuf_progress = True
|
||||
else:
|
||||
insuf_progress = False
|
||||
|
||||
# Evaluate new point
|
||||
f_new, g_new = obj_func(x, t, d)
|
||||
ls_func_evals += 1
|
||||
gtd_new = g_new.dot(d)
|
||||
ls_iter += 1
|
||||
|
||||
if f_new > (f + c1 * t * gtd) or f_new >= bracket_f[low_pos]:
|
||||
# Armijo condition not satisfied or not lower than lowest point
|
||||
bracket[high_pos] = t
|
||||
bracket_f[high_pos] = f_new
|
||||
bracket_g[high_pos] = g_new.clone(memory_format=torch.contiguous_format)
|
||||
bracket_gtd[high_pos] = gtd_new
|
||||
low_pos, high_pos = (0, 1) if bracket_f[0] <= bracket_f[1] else (1, 0)
|
||||
else:
|
||||
if abs(gtd_new) <= -c2 * gtd:
|
||||
# Wolfe conditions satisfied
|
||||
done = True
|
||||
elif gtd_new * (bracket[high_pos] - bracket[low_pos]) >= 0:
|
||||
# old high becomes new low
|
||||
bracket[high_pos] = bracket[low_pos]
|
||||
bracket_f[high_pos] = bracket_f[low_pos]
|
||||
bracket_g[high_pos] = bracket_g[low_pos]
|
||||
bracket_gtd[high_pos] = bracket_gtd[low_pos]
|
||||
|
||||
# new point becomes new low
|
||||
bracket[low_pos] = t
|
||||
bracket_f[low_pos] = f_new
|
||||
bracket_g[low_pos] = g_new.clone(memory_format=torch.contiguous_format)
|
||||
bracket_gtd[low_pos] = gtd_new
|
||||
|
||||
# return stuff
|
||||
t = bracket[low_pos]
|
||||
f_new = bracket_f[low_pos]
|
||||
g_new = bracket_g[low_pos]
|
||||
return f_new, g_new, t, ls_func_evals
|
||||
|
||||
|
||||
class MyLBFGS(Optimizer):
|
||||
"""Implements L-BFGS algorithm, heavily inspired by `minFunc
|
||||
<https://www.cs.ubc.ca/~schmidtm/Software/minFunc.html>`_.
|
||||
.. warning::
|
||||
This optimizer doesn't support per-parameter options and parameter
|
||||
groups (there can be only one).
|
||||
.. warning::
|
||||
Right now all parameters have to be on a single device. This will be
|
||||
improved in the future.
|
||||
.. note::
|
||||
This is a very memory intensive optimizer (it requires additional
|
||||
``param_bytes * (history_size + 1)`` bytes). If it doesn't fit in memory
|
||||
try reducing the history size, or use a different algorithm.
|
||||
Args:
|
||||
lr (float): learning rate (default: 1)
|
||||
max_iter (int): maximal number of iterations per optimization step
|
||||
(default: 20)
|
||||
max_eval (int): maximal number of function evaluations per optimization
|
||||
step (default: max_iter * 1.25).
|
||||
tolerance_grad (float): termination tolerance on first order optimality
|
||||
(default: 1e-5).
|
||||
tolerance_change (float): termination tolerance on function
|
||||
value/parameter changes (default: 1e-9).
|
||||
history_size (int): update history size (default: 100).
|
||||
line_search_fn (str): either 'strong_wolfe' or None (default: None).
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
params,
|
||||
lr=1,
|
||||
max_iter=20,
|
||||
max_eval=None,
|
||||
tolerance_grad=1e-7,
|
||||
tolerance_change=1e-9,
|
||||
history_size=100,
|
||||
line_search_fn=None):
|
||||
if max_eval is None:
|
||||
max_eval = max_iter * 5 // 4
|
||||
defaults = dict(
|
||||
lr=lr,
|
||||
max_iter=max_iter,
|
||||
max_eval=max_eval,
|
||||
tolerance_grad=tolerance_grad,
|
||||
tolerance_change=tolerance_change,
|
||||
history_size=history_size,
|
||||
line_search_fn=line_search_fn)
|
||||
super(MyLBFGS, self).__init__(params, defaults)
|
||||
|
||||
if len(self.param_groups) != 1:
|
||||
raise ValueError("LBFGS doesn't support per-parameter options "
|
||||
"(parameter groups)")
|
||||
|
||||
self._params = self.param_groups[0]['params']
|
||||
self._numel_cache = None
|
||||
|
||||
def _numel(self):
|
||||
if self._numel_cache is None:
|
||||
self._numel_cache = reduce(lambda total, p: total + p.numel(), self._params, 0)
|
||||
return self._numel_cache
|
||||
|
||||
def _gather_flat_grad(self):
|
||||
views = []
|
||||
for p in self._params:
|
||||
if p.grad is None:
|
||||
view = p.new(p.numel()).zero_()
|
||||
elif p.grad.is_sparse:
|
||||
view = p.grad.to_dense().view(-1)
|
||||
else:
|
||||
view = p.grad.view(-1)
|
||||
views.append(view)
|
||||
return torch.cat(views, 0)
|
||||
|
||||
def _add_grad(self, step_size, update):
|
||||
offset = 0
|
||||
for p in self._params:
|
||||
numel = p.numel()
|
||||
if ((update.dtype==torch.complex64 or update.dtype==torch.complex128)
|
||||
and (p.dtype==torch.float32 or p.dtype==torch.float64)):
|
||||
p.add_(update[offset:offset + numel].real.view_as(p), alpha=step_size)
|
||||
else:
|
||||
p.add_(update[offset:offset + numel].view_as(p), alpha=step_size)
|
||||
# view as to avoid deprecated pointwise semantics
|
||||
#try:
|
||||
# print(p.dtype)
|
||||
# print(update.dtype)
|
||||
# p.add_(update[offset:offset + numel].view_as(p), alpha=step_size)
|
||||
# print('Worked fine')
|
||||
# print(update[offset:offset + numel].view_as(p))
|
||||
#except:
|
||||
# print('Failed')
|
||||
# print(update[offset:offset + numel].view_as(p))
|
||||
# exit()
|
||||
offset += numel
|
||||
assert offset == self._numel()
|
||||
|
||||
def _clone_param(self):
|
||||
return [p.clone(memory_format=torch.contiguous_format) for p in self._params]
|
||||
|
||||
def _set_param(self, params_data):
|
||||
for p, pdata in zip(self._params, params_data):
|
||||
p.copy_(pdata)
|
||||
|
||||
def _directional_evaluate(self, closure, x, t, d):
|
||||
self._add_grad(t, d)
|
||||
loss = float(closure())
|
||||
flat_grad = self._gather_flat_grad()
|
||||
self._set_param(x)
|
||||
return loss, flat_grad
|
||||
|
||||
@torch.no_grad()
|
||||
def step(self, closure):
|
||||
"""Performs a single optimization step.
|
||||
Args:
|
||||
closure (callable): A closure that reevaluates the model
|
||||
and returns the loss.
|
||||
"""
|
||||
assert len(self.param_groups) == 1
|
||||
|
||||
# Make sure the closure is always called with grad enabled
|
||||
closure = torch.enable_grad()(closure)
|
||||
|
||||
group = self.param_groups[0]
|
||||
lr = group['lr']
|
||||
max_iter = group['max_iter']
|
||||
max_eval = group['max_eval']
|
||||
tolerance_grad = group['tolerance_grad']
|
||||
tolerance_change = group['tolerance_change']
|
||||
line_search_fn = group['line_search_fn']
|
||||
history_size = group['history_size']
|
||||
|
||||
# NOTE: LBFGS has only global state, but we register it as state for
|
||||
# the first param, because this helps with casting in load_state_dict
|
||||
state = self.state[self._params[0]]
|
||||
state.setdefault('func_evals', 0)
|
||||
state.setdefault('n_iter', 0)
|
||||
|
||||
# evaluate initial f(x) and df/dx
|
||||
orig_loss = closure()
|
||||
loss = float(orig_loss)
|
||||
current_evals = 1
|
||||
state['func_evals'] += 1
|
||||
|
||||
flat_grad = self._gather_flat_grad()
|
||||
opt_cond = flat_grad.abs().max() <= tolerance_grad
|
||||
|
||||
# optimal condition
|
||||
if opt_cond:
|
||||
return orig_loss
|
||||
|
||||
# tensors cached in state (for tracing)
|
||||
d = state.get('d')
|
||||
t = state.get('t')
|
||||
old_dirs = state.get('old_dirs')
|
||||
old_stps = state.get('old_stps')
|
||||
ro = state.get('ro')
|
||||
H_diag = state.get('H_diag')
|
||||
prev_flat_grad = state.get('prev_flat_grad')
|
||||
prev_loss = state.get('prev_loss')
|
||||
|
||||
n_iter = 0
|
||||
# optimize for a max of max_iter iterations
|
||||
while n_iter < max_iter:
|
||||
# keep track of nb of iterations
|
||||
n_iter += 1
|
||||
state['n_iter'] += 1
|
||||
|
||||
############################################################
|
||||
# compute gradient descent direction
|
||||
############################################################
|
||||
if state['n_iter'] == 1:
|
||||
d = flat_grad.neg()
|
||||
old_dirs = []
|
||||
old_stps = []
|
||||
ro = []
|
||||
H_diag = 1
|
||||
else:
|
||||
# do lbfgs update (update memory)
|
||||
y = flat_grad.sub(prev_flat_grad)
|
||||
s = d.mul(t)
|
||||
ys = y.dot(s) # y*s
|
||||
if ys.abs() > 1e-10:
|
||||
# updating memory
|
||||
if len(old_dirs) == history_size:
|
||||
# shift history by one (limited-memory)
|
||||
old_dirs.pop(0)
|
||||
old_stps.pop(0)
|
||||
ro.pop(0)
|
||||
|
||||
# store new direction/step
|
||||
old_dirs.append(y)
|
||||
old_stps.append(s)
|
||||
ro.append(1. / ys)
|
||||
|
||||
# update scale of initial Hessian approximation
|
||||
H_diag = ys / y.dot(y) # (y*y)
|
||||
|
||||
# compute the approximate (L-BFGS) inverse Hessian
|
||||
# multiplied by the gradient
|
||||
num_old = len(old_dirs)
|
||||
|
||||
if 'al' not in state:
|
||||
state['al'] = [None] * history_size
|
||||
al = state['al']
|
||||
|
||||
# iteration in L-BFGS loop collapsed to use just one buffer
|
||||
q = flat_grad.neg()
|
||||
for i in range(num_old - 1, -1, -1):
|
||||
al[i] = old_stps[i].dot(q) * ro[i]
|
||||
q.add_(old_dirs[i], alpha=-al[i])
|
||||
|
||||
# multiply by initial Hessian
|
||||
# r/d is the final direction
|
||||
d = r = torch.mul(q, H_diag)
|
||||
for i in range(num_old):
|
||||
be_i = old_dirs[i].dot(r) * ro[i]
|
||||
r.add_(old_stps[i], alpha=al[i] - be_i)
|
||||
|
||||
if prev_flat_grad is None:
|
||||
prev_flat_grad = flat_grad.clone(memory_format=torch.contiguous_format)
|
||||
else:
|
||||
prev_flat_grad.copy_(flat_grad)
|
||||
prev_loss = loss
|
||||
|
||||
############################################################
|
||||
# compute step length
|
||||
############################################################
|
||||
# reset initial guess for step size
|
||||
if state['n_iter'] == 1:
|
||||
t = min(1., 1. / flat_grad.abs().sum()) * lr
|
||||
else:
|
||||
t = lr
|
||||
|
||||
# directional derivative
|
||||
gtd = flat_grad.dot(d) # g * d
|
||||
# Why did this used to be gtd > -tol_change?
|
||||
|
||||
# directional derivative is below tolerance
|
||||
if gtd.abs() < tolerance_change:
|
||||
break
|
||||
|
||||
# optional line search: user function
|
||||
ls_func_evals = 0
|
||||
if line_search_fn is not None:
|
||||
# perform line search, using user function
|
||||
if line_search_fn != "strong_wolfe":
|
||||
raise RuntimeError("only 'strong_wolfe' is supported")
|
||||
else:
|
||||
x_init = self._clone_param()
|
||||
|
||||
def obj_func(x, t, d):
|
||||
return self._directional_evaluate(closure, x, t, d)
|
||||
|
||||
loss, flat_grad, t, ls_func_evals = _strong_wolfe(
|
||||
obj_func, x_init, t, d, loss, flat_grad, gtd)
|
||||
self._add_grad(t, d)
|
||||
opt_cond = flat_grad.abs().max() <= tolerance_grad
|
||||
else:
|
||||
# no line search, simply move with fixed-step
|
||||
self._add_grad(t, d)
|
||||
if n_iter != max_iter:
|
||||
# re-evaluate function only if not in last iteration
|
||||
# the reason we do this: in a stochastic setting,
|
||||
# no use to re-evaluate that function here
|
||||
with torch.enable_grad():
|
||||
loss = float(closure())
|
||||
flat_grad = self._gather_flat_grad()
|
||||
opt_cond = flat_grad.abs().max() <= tolerance_grad
|
||||
ls_func_evals = 1
|
||||
|
||||
# update func eval
|
||||
current_evals += ls_func_evals
|
||||
state['func_evals'] += ls_func_evals
|
||||
|
||||
############################################################
|
||||
# check conditions
|
||||
############################################################
|
||||
if n_iter == max_iter:
|
||||
break
|
||||
|
||||
if current_evals >= max_eval:
|
||||
break
|
||||
|
||||
# optimal condition
|
||||
if opt_cond:
|
||||
break
|
||||
|
||||
# lack of progress
|
||||
if d.mul(t).abs().max() <= tolerance_change:
|
||||
break
|
||||
|
||||
if abs(loss - prev_loss) < tolerance_change:
|
||||
break
|
||||
|
||||
state['d'] = d
|
||||
state['t'] = t
|
||||
state['old_dirs'] = old_dirs
|
||||
state['old_stps'] = old_stps
|
||||
state['ro'] = ro
|
||||
state['H_diag'] = H_diag
|
||||
state['prev_flat_grad'] = prev_flat_grad
|
||||
state['prev_loss'] = prev_loss
|
||||
|
||||
return orig_loss
|
||||
@@ -0,0 +1,647 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools import analysis
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from scipy import linalg as sla
|
||||
from copy import copy
|
||||
|
||||
__all__ = ['FancyPtycho']
|
||||
|
||||
|
||||
class FancyPtycho(CDIModel):
|
||||
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis,
|
||||
probe_guess,
|
||||
obj_guess,
|
||||
detector_slice=None,
|
||||
surface_normal=t.tensor([0., 0., 1.], dtype=t.float32),
|
||||
min_translation=t.tensor([0, 0], dtype=t.float32),
|
||||
background=None,
|
||||
translation_offsets=None,
|
||||
mask=None,
|
||||
weights=None,
|
||||
translation_scale=1,
|
||||
saturation=None,
|
||||
probe_support=None,
|
||||
oversampling=1,
|
||||
fourier_probe=False,
|
||||
loss='amplitude mse',
|
||||
units='um',
|
||||
simulate_probe_translation=False
|
||||
):
|
||||
|
||||
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.min_translation = t.tensor(min_translation)
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.detector_slice = copy(detector_slice)
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
self.units = units
|
||||
self.fourier_probe = fourier_probe
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.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)
|
||||
|
||||
# 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]))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
dummy_det = t.empty([s//oversampling
|
||||
for s in self.probe[0].shape])
|
||||
shape = dummy_det[self.detector_slice].shape
|
||||
#shape = self.probe[0][self.detector_slice].shape
|
||||
else:
|
||||
shape = [s//oversampling for s in self.probe[0]]
|
||||
background = 1e-6 * t.ones(shape, dtype=t.float32)
|
||||
|
||||
self.background = t.nn.Parameter(background)
|
||||
|
||||
if weights is None:
|
||||
self.weights = None
|
||||
else:
|
||||
# We now need to distinguish between real-valued per-image
|
||||
# weights and complex-valued per-mode weight matrices
|
||||
if len(weights.shape) == 1:
|
||||
# This is if it's just a list of numbers
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.float32))
|
||||
else:
|
||||
# Now this is a matrix of weights, so it needs to be complex
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.complex64))
|
||||
|
||||
if translation_offsets is None:
|
||||
self.translation_offsets = None
|
||||
else:
|
||||
t_o = t.tensor(translation_offsets, dtype=t.float32)
|
||||
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.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, Js = t.meshgrid(Is/t.max(Is), Js/t.max(Js))
|
||||
self.I_phase = 2 * np.pi* Is
|
||||
self.J_phase = 2 * np.pi* Js
|
||||
|
||||
# Here we set the appropriate loss function
|
||||
if (loss.lower().strip() == 'amplitude mse'
|
||||
or loss.lower().strip() == 'amplitude_mse'):
|
||||
self.loss = tools.losses.amplitude_mse
|
||||
elif (loss.lower().strip() == 'poisson nll'
|
||||
or loss.lower().strip() == 'poisson_nll'):
|
||||
self.loss = tools.losses.poisson_nll
|
||||
else:
|
||||
raise KeyError('Specified loss function not supported')
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls,
|
||||
dataset,
|
||||
probe_size=None,
|
||||
randomize_ang=0,
|
||||
padding=0,
|
||||
n_modes=1,
|
||||
dm_rank=None,
|
||||
translation_scale=1,
|
||||
saturation=None,
|
||||
probe_support_radius=None,
|
||||
propagation_distance=None,
|
||||
scattering_mode=None,
|
||||
oversampling=1,
|
||||
auto_center=False,
|
||||
opt_for_fft=False,
|
||||
fourier_probe=False,
|
||||
loss='amplitude mse',
|
||||
units='um',
|
||||
simulate_probe_translation=False
|
||||
):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
|
||||
# We include the *extras to make this work even with datasets, like
|
||||
# polarization dependent datasets, that might toss out extra inputs
|
||||
(indices, translations, *extras), patterns = dataset[:]
|
||||
|
||||
dataset.get_as(*get_as_args[0], **get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns, dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:, 0], det_basis[:, 1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0., 0., 1.])
|
||||
surface_normal /= -np.linalg.norm(surface_normal)
|
||||
|
||||
# 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, padding=200)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
if probe_size is None:
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
else:
|
||||
probe = tools.initializers.gaussian_probe(dataset, probe_basis, probe_shape, probe_size, propagation_distance=propagation_distance)
|
||||
|
||||
# Now we initialize all the subdominant probe modes
|
||||
probe_max = t.max(t.abs(probe))
|
||||
probe_stack = [0.01 * probe_max * t.rand(probe.shape, dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
|
||||
# For a Fourier space probe
|
||||
if fourier_probe:
|
||||
probe = tools.propagators.far_field(probe)
|
||||
|
||||
probe = t.stack([probe, ] + probe_stack)
|
||||
|
||||
obj = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset), 2)) - 0.5)
|
||||
|
||||
if dm_rank is not None and dm_rank != 0:
|
||||
if dm_rank > n_modes:
|
||||
raise KeyError('Density matrix rank cannot be greater than the number of modes. Use dm_rank = -1 to use a full rank matrix.')
|
||||
elif dm_rank == -1:
|
||||
# dm_rank == -1 is defined to mean full-rank
|
||||
dm_rank = n_modes
|
||||
|
||||
Ws = t.zeros(len(dataset), dm_rank, n_modes, dtype=t.complex64)
|
||||
# Start with as close to the identity matrix as possible,
|
||||
# cutting of when we hit the specified maximum rank
|
||||
for i in range(0, dm_rank):
|
||||
Ws[:, i, i] = 1
|
||||
else:
|
||||
# dm_rank == None or dm_rank = 0 triggers a special case where
|
||||
# a standard incoherent multi-mode model is used. This is the
|
||||
# default, because it is so common.
|
||||
# In this case, we define a set of weights which only has one index
|
||||
Ws = t.ones(len(dataset))
|
||||
|
||||
if hasattr(dataset, 'intensities') and dataset.intensities is not None:
|
||||
Ws *= (dataset.intensities.to(dtype=Ws.dtype)[:,...]
|
||||
/ t.mean(dataset.intensities))
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
if probe_support_radius is not None:
|
||||
probe_support = t.zeros(probe[0].shape, dtype=t.bool)
|
||||
xs, ys = np.mgrid[:probe.shape[-2], :probe.shape[-1]]
|
||||
xs = xs - np.mean(xs)
|
||||
ys = ys - np.mean(ys)
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
probe_support[Rs < probe_support_radius] = 1
|
||||
probe = probe * probe_support[None, :, :]
|
||||
|
||||
else:
|
||||
probe_support = None
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, probe, obj,
|
||||
detector_slice=det_slice,
|
||||
surface_normal=surface_normal,
|
||||
min_translation=min_translation,
|
||||
translation_offsets=translation_offsets,
|
||||
weights=Ws, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
probe_support=probe_support,
|
||||
fourier_probe=fourier_probe,
|
||||
oversampling=oversampling,
|
||||
loss=loss, units=units,
|
||||
simulate_probe_translation=simulate_probe_translation)
|
||||
|
||||
|
||||
def interaction(self, index, translations, *args):
|
||||
|
||||
# The *args is included so that this can work even when given, say,
|
||||
# a polarized ptycho dataset that might spit out more inputs.
|
||||
|
||||
# Step 1 is to convert the translations for each position into a
|
||||
# value in pixels
|
||||
pix_trans = tools.interactions.translations_to_pixel(
|
||||
self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans -= self.min_translation
|
||||
# We then add on any recovered translation offset, if they exist
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += (self.translation_scale *
|
||||
self.translation_offsets[index])
|
||||
|
||||
# This restricts the basis probes to stay within the probe support
|
||||
basis_prs = self.probe * self.probe_support[..., :, :]
|
||||
|
||||
# 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]
|
||||
else:
|
||||
try:
|
||||
Ws = t.ones(len(index)) # I'm positive this introduced a bug
|
||||
except:
|
||||
Ws = 1
|
||||
|
||||
if self.weights is None or len(self.weights[0].shape) == 0:
|
||||
# If a purely stable coherent illumination is defined
|
||||
prs = Ws[..., None, None, None] * basis_prs
|
||||
else:
|
||||
# If a frame-by-frame weight matrix is defined
|
||||
# This takes the dot product of all the weight matrices with
|
||||
# the probes. The output has dimensions of translation, then
|
||||
# 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'],
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
|
||||
probe_masks = t.exp(1j* (det_pix_trans[:,0,None,None] *
|
||||
self.I_phase[None,...] +
|
||||
det_pix_trans[:,1,None,None] *
|
||||
self.J_phase[None,...]))
|
||||
prs = prs * probe_masks[...,None,:,:]
|
||||
|
||||
|
||||
# Now we actually do the interaction, using the sinc subpixel
|
||||
# translation model as per usual
|
||||
exit_waves = self.probe_norm * tools.interactions.ptycho_2D_sinc(
|
||||
prs, self.obj, pix_trans,
|
||||
shift_probe=True, multiple_modes=True)
|
||||
return exit_waves
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.quadratic_background(
|
||||
wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
|
||||
# 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.)
|
||||
|
||||
# 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
|
||||
|
||||
data = []
|
||||
len(indices)
|
||||
if calculation_width is None:
|
||||
calculation_width = len(indices)
|
||||
index_chunks = [indices[i:i + calculation_width]
|
||||
for i in range(0, len(indices),
|
||||
calculation_width)]
|
||||
translation_chunks = [translations[i:i + calculation_width]
|
||||
for i in range(0, len(indices),
|
||||
calculation_width)]
|
||||
|
||||
|
||||
# Then we simulate the results
|
||||
data = [self.forward(idx, trans).detach()
|
||||
for idx, trans in zip(index_chunks, translation_chunks)]
|
||||
|
||||
data = t.cat(data, dim=0)
|
||||
# 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)
|
||||
|
||||
|
||||
def corrected_translations(self, dataset):
|
||||
translations = dataset.translations.to(
|
||||
dtype=t.float32, device=self.probe.device)
|
||||
if (hasattr(self, 'translation_offsets') and
|
||||
self.translation_offsets is not None):
|
||||
t_offset = tools.interactions.pixel_to_translations(
|
||||
self.probe_basis,
|
||||
self.translation_offsets * self.translation_scale,
|
||||
surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
else:
|
||||
return translations
|
||||
|
||||
|
||||
def get_rhos(self):
|
||||
# If this is the general unified mode model
|
||||
if self.weights.dim() >= 2:
|
||||
Ws = self.weights.detach().cpu().numpy()
|
||||
rhos_out = np.matmul(np.swapaxes(Ws, 1, 2), Ws.conj())
|
||||
return rhos_out
|
||||
# This is the purely incoherent case
|
||||
else:
|
||||
return np.array([np.eye(self.probe.shape[0])]*self.weights.shape[0],
|
||||
dtype=np.complex64)
|
||||
|
||||
def tidy_probes(self, normalization=1, normalize=False):
|
||||
"""Tidies up the probes
|
||||
|
||||
What we want to do here is use all the information on all the probes
|
||||
to calculate a natural basis for the experiment, and update all the
|
||||
density matrices to operate in that updated basis
|
||||
"""
|
||||
|
||||
# First we treat the purely incoherent case
|
||||
|
||||
# I don't love this pattern of using an if statement with a return
|
||||
# to catch this case, but because it's so much simpler than the
|
||||
# unified mode case I think it's appropriate
|
||||
if self.weights.dim() == 1:
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes = analysis.orthogonalize_probes(probe)
|
||||
self.probe.data = t.as_tensor(
|
||||
ortho_probes,
|
||||
device=self.probe.device,
|
||||
dtype=self.probe.dtype)
|
||||
return
|
||||
|
||||
# This is for the unified mode case
|
||||
|
||||
# Note to future: We could probably do this more cleanly with an
|
||||
# SVD directly on the Ws matrix, instead of an eigendecomposition
|
||||
# of the rho matrix.
|
||||
|
||||
rhos = self.get_rhos()
|
||||
overall_rho = np.mean(rhos, axis=0)
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes, A = analysis.orthogonalize_probes(
|
||||
probe, density_matrix=overall_rho,
|
||||
keep_transform=True, normalize=normalize)
|
||||
Aconj = A.conj()
|
||||
Atrans = np.transpose(A)
|
||||
new_rhos = np.matmul(Atrans, np.matmul(rhos, Aconj))
|
||||
|
||||
new_rhos /= normalization
|
||||
ortho_probes *= np.sqrt(normalization)
|
||||
|
||||
dm_rank = self.weights.shape[1]
|
||||
|
||||
new_Ws = []
|
||||
for rho in new_rhos:
|
||||
# These are returned from smallest to largest - we want to keep
|
||||
# the largest ones
|
||||
w, v = sla.eigh(rho)
|
||||
w = w[::-1][:dm_rank]
|
||||
v = v[:, ::-1][:, :dm_rank]
|
||||
# For situations where the rank of the density matrix is not
|
||||
# full in reality, but we keep more modes around than needed,
|
||||
# some ws can go negative due to numerical error! This is
|
||||
# extremely rare, but comon enough to cause crashes occasionally
|
||||
# when there are thousands of individual matrices to transform
|
||||
# every time this is called.
|
||||
w = np.maximum(w, 0)
|
||||
|
||||
new_Ws.append(np.dot(np.diag(np.sqrt(w)), v.transpose()))
|
||||
|
||||
new_Ws = np.array(new_Ws)
|
||||
|
||||
self.weights.data = t.as_tensor(
|
||||
new_Ws, dtype=self.weights.dtype, device=self.weights.device)
|
||||
|
||||
self.probe.data = t.as_tensor(
|
||||
ortho_probes, device=self.probe.device, dtype=self.probe.dtype)
|
||||
|
||||
|
||||
def plot_wavefront_variation(self, dataset, fig=None, mode='amplitude', **kwargs):
|
||||
def get_probes(idx):
|
||||
basis_prs = self.probe * self.probe_support[..., :, :]
|
||||
prs = t.sum(self.weights[idx, :, :, None, None] * basis_prs,
|
||||
axis=-4)
|
||||
ortho_probes = analysis.orthogonalize_probes(prs)
|
||||
|
||||
if mode.lower() == 'amplitude':
|
||||
return np.abs(ortho_probes.detach().cpu().numpy())
|
||||
if mode.lower() == 'root_sum_intensity':
|
||||
return np.sum(np.abs(ortho_probes.detach().cpu().numpy())**2,
|
||||
axis=0)
|
||||
if mode.lower() == 'phase':
|
||||
return np.angle(ortho_probes.detach().cpu().numpy())
|
||||
|
||||
probe_matrix = np.zeros([self.probe.shape[0]]*2,
|
||||
dtype=np.complex64)
|
||||
np_probes = self.probe.detach().cpu().numpy()
|
||||
for i in range(probe_matrix.shape[0]):
|
||||
for j in range(probe_matrix.shape[0]):
|
||||
probe_matrix[i,j] = np.sum(np_probes[i]*np_probes[j].conj())
|
||||
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
|
||||
probe_intensities = np.sum(np.tensordot(weights, probe_matrix, axes=1)
|
||||
* weights.conj(), axis=2)
|
||||
|
||||
# Imaginary part is already essentially zero up to rounding error
|
||||
probe_intensities = np.real(probe_intensities)
|
||||
|
||||
values = np.sum(probe_intensities, axis=1)
|
||||
if mode.lower() == 'amplitude' or mode.lower() == 'root_sum_intensity':
|
||||
cmap = 'viridis'
|
||||
else:
|
||||
cmap = 'twilight'
|
||||
|
||||
p.plot_nanomap_with_images(self.corrected_translations(dataset), get_probes, values=values, fig=fig, units=self.units, basis=self.probe_basis, nanomap_colorbar_title='Total Probe Intensity', cmap=cmap, **kwargs),
|
||||
|
||||
|
||||
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: 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: 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: 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)),
|
||||
('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)),
|
||||
('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)),
|
||||
('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)),
|
||||
('Average Density Matrix Amplitudes',
|
||||
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: len(self.weights.shape) >= 2),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, 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)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig, units=self.units)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2))
|
||||
]
|
||||
|
||||
# def plot_errors(self, dataset):
|
||||
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
|
||||
return {'basis': basis, 'translation': translations,
|
||||
'probe': probe, 'obj': obj,
|
||||
'background': background,
|
||||
'weights': weights}
|
||||
@@ -0,0 +1,359 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools.interactions import RPI_interaction
|
||||
from CDTools.tools import initializers
|
||||
from scipy.ndimage.morphology import binary_dilation
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
|
||||
__all__ = ['MultimodeRPI']
|
||||
|
||||
|
||||
|
||||
__all__ = ['RPI']
|
||||
|
||||
class MultimodeRPI(CDIModel):
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
return t.complex(self.obj_real, self.obj_imag)
|
||||
|
||||
@property
|
||||
def weights(self):
|
||||
ws = t.complex(self.weights_real, self.weights_imag)
|
||||
return ws / 10# / self.obj_real.size().numel()
|
||||
|
||||
def __init__(self, wavelength, detector_geometry, probe_basis,
|
||||
probe, obj_guess, detector_slice=None,
|
||||
background=None, mask=None, saturation=None,
|
||||
obj_support=None, oversampling=1, weight_matrix=False):
|
||||
|
||||
super(MultimodeRPI, self).__init__()
|
||||
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
|
||||
scale_factor = t.tensor([probe.shape[-1]/obj_guess.shape[-1],
|
||||
probe.shape[-2]/obj_guess.shape[-2]])
|
||||
self.obj_basis = self.probe_basis * scale_factor
|
||||
self.detector_slice = detector_slice
|
||||
|
||||
# Maybe something to include in a bit
|
||||
# self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.mask = t.tensor(mask, dtype=t.bool)
|
||||
|
||||
|
||||
self.probe = t.tensor(probe, dtype=t.complex64)
|
||||
|
||||
obj_guess = t.tensor(obj_guess, dtype=t.complex64)
|
||||
|
||||
self.obj_real = t.nn.Parameter(obj_guess.real)
|
||||
self.obj_imag = t.nn.Parameter(obj_guess.imag)
|
||||
|
||||
self.weights_real = t.nn.Parameter(t.eye(probe.shape[0])* 10)# * self.obj_real.size().numel())
|
||||
self.weights_imag = t.nn.Parameter(t.zeros(probe.shape[0]))
|
||||
|
||||
if not weight_matrix:
|
||||
self.weights_real.requires_grad=False
|
||||
self.weights_imag.requires_grad=False
|
||||
|
||||
# Wait for LBFGS to be updated for complex-valued parameters
|
||||
# self.obj = t.nn.Parameter(obj_guess.to(t.float32))
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[0][self.detector_slice].shape,
|
||||
dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.tensor(background, dtype=t.float32)
|
||||
|
||||
if obj_support is not None:
|
||||
self.obj_support = obj_support
|
||||
self.obj.data = self.obj * obj_support[None, ...]
|
||||
else:
|
||||
self.obj_support = t.ones_like(self.obj[0, ...])
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe, obj_size=None, background=None, mask=None, padding=0, n_modes=1, saturation=None, scattering_mode=None, oversampling=1, auto_center=False, initialization='random', opt_for_fft=False, weight_matrix=False, probe_threshold=0):
|
||||
raise NotImplementedError()
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
# We only need the patterns here, not the inputs associated with them.
|
||||
_, patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns,dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
if not isinstance(probe,t.Tensor):
|
||||
probe = t.as_tensor(probe)
|
||||
|
||||
# Potentially need all of this orientation stuff later
|
||||
|
||||
#if hasattr(dataset, 'sample_info') and \
|
||||
# dataset.sample_info is not None and \
|
||||
# 'orientation' in dataset.sample_info:
|
||||
# surface_normal = dataset.sample_info['orientation'][2]
|
||||
#else:
|
||||
# surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
#if scattering_mode in {'t', 'transmission'}:
|
||||
# surface_normal = np.array([0.,0.,1.])
|
||||
#elif scattering_mode in {'r', 'reflection'}:
|
||||
# outgoing_dir = np.cross(det_basis[:,0], det_basis[:,1])
|
||||
# outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
# surface_normal = outgoing_dir + np.array([0.,0.,1.])
|
||||
# surface_normal /= np.linalg.norm(surface_normal)
|
||||
|
||||
|
||||
if background is None and hasattr(dataset, 'background') \
|
||||
and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
elif background is not None:
|
||||
background = t.sqrt(t.Tensor(background).to(dtype=t.float32))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
# If no mask is given, but one exists in the dataset, load it.
|
||||
if mask is None and hasattr(dataset, 'mask') \
|
||||
and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
|
||||
# Now we initialize the object
|
||||
if obj_size is None:
|
||||
# This is a standard size for a well-matched probe and detector
|
||||
obj_size = (np.array(probe_shape) // 2).astype(int)
|
||||
|
||||
if initialization.lower().strip() == 'random':
|
||||
# I think something to do with the fact that the object is defined
|
||||
# on a coarser grid needs to be accounted for here that is not
|
||||
# accounted for yet
|
||||
scale = t.sum(patterns[0]) / t.sum(t.abs(probe)**2)
|
||||
obj_guess = scale * t.exp(2j * np.pi * t.rand([n_modes,]+obj_size))
|
||||
elif initialization.lower().strip() == 'spectral':
|
||||
if background is not None:
|
||||
obj_guess = initializers.RPI_spectral_init(
|
||||
patterns[0], probe, obj_size, mask=mask,
|
||||
background=background**2, n_modes=n_modes)
|
||||
else:
|
||||
obj_guess = initializers.RPI_spectral_init(
|
||||
patterns[0], probe, obj_size, mask=mask,
|
||||
n_modes=n_modes)
|
||||
|
||||
else:
|
||||
raise KeyError('Initialization "' + str(initialization) + \
|
||||
'" invalid - use "spectral" or "random"')
|
||||
|
||||
probe_intensity = t.sqrt(t.sum(t.abs(probe)**2,axis=0))
|
||||
probe_fft = tools.propagators.far_field(probe_intensity)
|
||||
pad0l = (probe.shape[-2] - obj_size[-2])//2
|
||||
pad0r = probe.shape[-2] - obj_size[-2] - pad0l
|
||||
pad1l = (probe.shape[-1] - obj_size[-1])//2
|
||||
pad1r = probe.shape[-1] - obj_size[-1] - pad1l
|
||||
probe_lr_fft = probe_fft[pad0l:-pad0r,pad1l:-pad1r]
|
||||
probe_lr = t.abs(tools.propagators.inverse_far_field(probe_lr_fft))
|
||||
|
||||
obj_support = probe_lr > t.max(probe_lr) * probe_threshold
|
||||
obj_support = t.as_tensor(binary_dilation(obj_support))
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis,
|
||||
probe, obj_guess, detector_slice=det_slice,
|
||||
background=background, mask=mask, saturation=saturation,
|
||||
obj_support=obj_support, oversampling=oversampling,
|
||||
weight_matrix=weight_matrix)
|
||||
|
||||
|
||||
def random_init(self, pattern):
|
||||
scale = t.sum(pattern) / t.sum(t.abs(self.probe)**2)
|
||||
self.obj.data = scale * t.exp(
|
||||
2j * np.pi * t.rand(self.obj.shape)).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
|
||||
def spectral_init(self, pattern):
|
||||
if self.background is not None:
|
||||
self.obj.data = initializers.RPI_spectral_init(
|
||||
pattern, self.probe, self.obj.shape[-3:-1], mask=self.mask,
|
||||
background=self.background**2, n_modes=self.obj.shape[0]).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
else:
|
||||
self.obj.data = initializers.RPI_spectral_init(
|
||||
pattern, self.probe, self.obj.shape[-3:-1], mask=self.mask,
|
||||
n_modes=self.obj.shape[0]).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
|
||||
# Needs work
|
||||
def interaction(self, index, *args):
|
||||
# including *args allows this to work with all sorts of datasets
|
||||
# that might include other information in with the index in their
|
||||
# "input" parameters (such as translations for a ptychography dataset).
|
||||
# This makes it seamless to use such a dataset even though those
|
||||
# extra arguments will not be used.
|
||||
|
||||
|
||||
all_exit_waves = []
|
||||
|
||||
# Mix the probes with the weight matrix
|
||||
prs = t.sum(self.weights[..., None, None] * self.probe, axis=-3)
|
||||
|
||||
for i in range(self.probe.shape[0]):
|
||||
pr = prs[i]
|
||||
# Here we have a 3D probe (one single mode)
|
||||
# and a 4D object (multiple modes mixing incoherently)
|
||||
exit_waves = RPI_interaction(pr,
|
||||
self.obj_support * self.obj[i])
|
||||
all_exit_waves.append(exit_waves.unsqueeze(0))
|
||||
|
||||
# This creates a bunch of modes generated from all possible combos
|
||||
# of the probe and object modes all strung out along the first index
|
||||
|
||||
output = t.cat(all_exit_waves)
|
||||
|
||||
# If we have multiple indexes input, we unsqueeze and repeat the stack
|
||||
# of wavefields enough times to simulate each requested index. This
|
||||
# seems silly, but it enables (for example) one to do a reconstruction
|
||||
# from a set of diffraction patterns that are all known to be from the
|
||||
# same object.
|
||||
try:
|
||||
# will fail if index has no length, for example when index
|
||||
# is just an int. In this case, we just do nothing instead
|
||||
output = output.unsqueeze(0).repeat(1,len(index),1,1,1)
|
||||
except TypeError:
|
||||
pass
|
||||
return output
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
# Here I'm taking advantage of an undocumented feature in the
|
||||
# incoherent_sum measurement function where it will work with
|
||||
# a 4D wavefield array as well as a 5D array.
|
||||
return tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
#return tools.losses.poisson_nll(real_data, sim_data, mask=mask)
|
||||
|
||||
def regularizer(self, factors):
|
||||
return factors[0] * t.sum(t.abs(self.obj[0,:,:])**2) \
|
||||
+ factors[1] * t.sum(t.abs(self.obj[1:,:,:])**2)
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(MultimodeRPI, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
|
||||
self.probe = self.probe.to(*args,**kwargs)
|
||||
self.probe_basis = self.probe_basis.to(*args,**kwargs)
|
||||
self.obj_basis = self.obj_basis.to(*args,**kwargs)
|
||||
self.obj_support = self.obj_support.to(*args,**kwargs)
|
||||
self.background = self.background.to(*args, **kwargs)
|
||||
|
||||
# Maybe include in a bit
|
||||
#self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
def sim_to_dataset(self, args_list):
|
||||
raise NotImplementedError('No sim to dataset yet, sorry!')
|
||||
|
||||
plot_list = [
|
||||
('Root Sum Squared Amplitude of all Probes',
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
np.sqrt(np.sum((t.abs(t.sum(self.weights[..., None, None].detach() * self.probe, axis=-3))**2).cpu().numpy(),axis=0)),
|
||||
fig=fig, basis=self.probe_basis)),
|
||||
('Object Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig,
|
||||
basis=self.obj_basis)),
|
||||
('Object Phases',
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig,
|
||||
basis=self.obj_basis))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset=None, full_obj=False):
|
||||
# dataset is set as a kwarg here because it isn't needed, but the
|
||||
# common pattern is to pass a dataset. This makes it okay if one
|
||||
# continues to use that standard pattern
|
||||
probe_basis = self.probe_basis.detach().cpu().numpy()
|
||||
obj_basis = self.obj_basis.detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
# Provide the option to save out the subdominant objects or
|
||||
# just the dominant one
|
||||
if full_obj:
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
else:
|
||||
obj = self.obj[0].detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
|
||||
return {'probe_basis': probe_basis, 'obj_basis': obj_basis,
|
||||
'probe': probe,'obj': obj,
|
||||
'background': background}
|
||||
|
||||
@@ -0,0 +1,637 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import analysis, image_processing
|
||||
from CDTools.tools import plotting as p
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
from functools import reduce
|
||||
|
||||
__all__ = ['Multislice2DPtycho']
|
||||
|
||||
|
||||
|
||||
class Multislice2DPtycho(CDIModel):
|
||||
|
||||
@property
|
||||
def probe(self):
|
||||
return t.complex(self.probe_real, self.probe_imag)
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
return t.complex(self.obj_real, self.obj_imag)
|
||||
|
||||
def __init__(self,
|
||||
wavelength,
|
||||
detector_geometry,
|
||||
probe_basis,
|
||||
probe_guess, obj_guess, dz, nz,
|
||||
detector_slice=None,
|
||||
surface_normal=np.array([0., 0., 1.]),
|
||||
min_translation=t.Tensor([0, 0]),
|
||||
background=None,
|
||||
translation_offsets=None,
|
||||
mask=None,
|
||||
weights=None,
|
||||
translation_scale=1,
|
||||
saturation=None,
|
||||
probe_support=None,
|
||||
oversampling=1,
|
||||
bandlimit=None,
|
||||
subpixel=True,
|
||||
exponentiate_obj=True,
|
||||
fourier_probe=False,
|
||||
prevent_aliasing=True,
|
||||
phase_only=False,
|
||||
units='um',
|
||||
):
|
||||
|
||||
super(Multislice2DPtycho, self).__init__()
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
self.dz = dz
|
||||
self.nz = nz
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.min_translation = t.tensor(min_translation)
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.detector_slice = copy(detector_slice)
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
self.subpixel = subpixel
|
||||
self.exponentiate_obj = exponentiate_obj
|
||||
self.fourier_probe = fourier_probe
|
||||
self.units = units
|
||||
self.phase_only = phase_only
|
||||
self.prevent_aliasing = prevent_aliasing
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.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)
|
||||
|
||||
# We rescale the probe here so it learns at the same rate as the
|
||||
# object
|
||||
if probe_guess.dim() > 3:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess[0]))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
|
||||
pg = probe_guess / self.probe_norm
|
||||
self.probe_real = t.nn.Parameter(pg.real)
|
||||
self.probe_imag = t.nn.Parameter(pg.imag)
|
||||
|
||||
self.obj_real = t.nn.Parameter(obj_guess.real)
|
||||
self.obj_imag = t.nn.Parameter(obj_guess.imag)
|
||||
|
||||
#self.probe = t.nn.Parameter(probe_guess.to(t.complex64)
|
||||
# / self.probe_norm)
|
||||
#self.obj = t.nn.Parameter(obj_guess.to(t.complex64))
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[0][self.detector_slice].shape, dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.nn.Parameter(background)
|
||||
|
||||
if weights is None:
|
||||
self.weights = None
|
||||
else:
|
||||
# We now need to distinguish between real-valued per-image
|
||||
# weights and complex-valued per-mode weight matrices
|
||||
if len(weights.shape) == 1:
|
||||
# This is if it's just a list of numbers
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.float32))
|
||||
else:
|
||||
# Now this is a matrix of weights, so it needs to be complex
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.complex64))
|
||||
|
||||
if translation_offsets is None:
|
||||
self.translation_offsets = None
|
||||
else:
|
||||
t_o = t.tensor(translation_offsets, dtype=t.float32)
|
||||
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 = t.tensor(probe_support, dtype=t.bool)
|
||||
else:
|
||||
self.probe_support = None
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
spacing = np.linalg.norm(self.probe_basis, axis=0)
|
||||
shape = np.array(self.probe.shape[1:])
|
||||
if prevent_aliasing:
|
||||
shape *= 2
|
||||
spacing /= 2
|
||||
|
||||
self.bandlimit = bandlimit
|
||||
|
||||
self.as_prop = tools.propagators.generate_angular_spectrum_propagator(shape, spacing, self.wavelength, self.dz, self.bandlimit)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, dz, nz, probe_convergence_semiangle, padding=0, n_modes=1, dm_rank=None, translation_scale=1, saturation=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=True, bandlimit=None, replicate_slice=False, subpixel=True, exponentiate_obj=True, units='um', fourier_probe=False, phase_only=False, prevent_aliasing=True, probe_support_radius=None):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
(indices, translations), patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0], **get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns, dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=False,
|
||||
oversampling=oversampling)
|
||||
|
||||
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:,0], det_basis[:,1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0.,0.,1.])
|
||||
surface_normal /= np.linalg.norm(outgoing_dir)
|
||||
|
||||
# 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, padding=100)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
probe = tools.initializers.STEM_style_probe(dataset, probe_shape, det_slice, probe_convergence_semiangle, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
#probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
|
||||
# Now we initialize all the subdominant probe modes
|
||||
probe_max = t.max(t.abs(probe))
|
||||
if n_modes >=2:
|
||||
probe_stack = list(0.01*tools.initializers.generate_subdominant_modes(probe,n_modes-1,circular=False))
|
||||
#probe_stack = [0.01 * probe_max * t.rand(probe.shape,dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
probe = t.stack([probe,] + probe_stack)
|
||||
else:
|
||||
probe = t.unsqueeze(probe,0)
|
||||
|
||||
# For a Fourier space probe
|
||||
if fourier_probe:
|
||||
probe = tools.propagators.far_field(probe)
|
||||
|
||||
# Consider a different start
|
||||
if exponentiate_obj:
|
||||
obj = t.zeros(obj_size, dtype=t.complex64)
|
||||
else:
|
||||
obj = t.exp(1j*t.zeros(obj_size))
|
||||
# If we will use a separate object per slice
|
||||
if not replicate_slice:
|
||||
obj = t.stack([obj]*nz)
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset),2)) - 0.5)
|
||||
|
||||
if dm_rank is not None and dm_rank != 0:
|
||||
if dm_rank > n_modes:
|
||||
raise KeyError('Density matrix rank cannot be greater than the number of modes. Use dm_rank = -1 to use a full rank matrix.')
|
||||
elif dm_rank == -1:
|
||||
# dm_rank == -1 is defined to mean full-rank
|
||||
dm_rank = n_modes
|
||||
Ws = t.zeros(len(dataset),dm_rank,n_modes,2)
|
||||
# Start with as close to the identity matrix as possible,
|
||||
# cutting of when we hit the specified maximum rank
|
||||
for i in range(0,dm_rank):
|
||||
Ws[:,i,i,0] = 1
|
||||
else:
|
||||
# dm_rank == None or dm_rank = 0 triggers a special case where
|
||||
# a standard incoherent multi-mode model is used. This is the
|
||||
# default, because it is so common.
|
||||
# In this case, we define a set of weights which only has one index
|
||||
Ws = t.ones(len(dataset))
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
if probe_support_radius is not None:
|
||||
probe_support = t.zeros(probe[0].shape, dtype=t.bool)
|
||||
xs, ys = np.mgrid[:probe.shape[-2],:probe.shape[-1]]
|
||||
xs = xs - np.mean(xs)
|
||||
ys = ys - np.mean(ys)
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
probe_support[Rs<probe_support_radius] = 1
|
||||
probe = probe * probe_support[None,:,:]
|
||||
|
||||
else:
|
||||
probe_support = None
|
||||
|
||||
probe = probe
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, probe, obj, dz, nz,
|
||||
detector_slice=det_slice,
|
||||
surface_normal=surface_normal,
|
||||
probe_support=probe_support,
|
||||
min_translation=min_translation,
|
||||
translation_offsets = translation_offsets,
|
||||
weights=Ws, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
oversampling=oversampling,
|
||||
bandlimit=bandlimit,
|
||||
subpixel=subpixel,
|
||||
exponentiate_obj=exponentiate_obj,
|
||||
units=units, fourier_probe=fourier_probe,
|
||||
phase_only=phase_only,
|
||||
prevent_aliasing=prevent_aliasing)
|
||||
|
||||
|
||||
def interaction(self, index, translations):
|
||||
pix_trans = tools.interactions.translations_to_pixel(self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans -= self.min_translation
|
||||
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += self.translation_scale * self.translation_offsets[index]
|
||||
|
||||
# This restricts the basis probes to stay within the probe support
|
||||
if self.probe_support is not None:
|
||||
basis_prs = self.probe * self.probe_support[...,:,:]
|
||||
else:
|
||||
basis_prs = self.probe
|
||||
|
||||
# For a Fourier-space probe, we take an IFT
|
||||
if self.fourier_probe:
|
||||
basis_prs = tools.propagators.inverse_far_field(basis_prs)
|
||||
|
||||
if self.prevent_aliasing:
|
||||
#pix_trans = pix_trans * 2
|
||||
basis_prs = image_processing.fourier_upsample(basis_prs)
|
||||
|
||||
# Now we construct the probes for each shot from the basis probes
|
||||
Ws = self.weights[index]
|
||||
|
||||
if len(self.weights[0].shape) == 0:
|
||||
# If a purely stable coherent illumination is defined
|
||||
# No cmult because Ws is real in this case
|
||||
prs = Ws[...,None,None,None] * basis_prs
|
||||
else:
|
||||
# If a frame-by-frame weight matrix is defined
|
||||
# This takes the dot product of all the weight matrices with
|
||||
# the probes. The output has dimensions of translation, then
|
||||
# coherent mode index, then x,y, and then complex index
|
||||
prs = t.sum(Ws[...,None,None] * basis_prs, axis=-3)
|
||||
|
||||
|
||||
if self.exponentiate_obj:
|
||||
if self.phase_only:
|
||||
obj = t.exp(1j*self.obj.real)
|
||||
else:
|
||||
obj = t.exp(1j*self.obj)
|
||||
else:
|
||||
obj = self.obj
|
||||
|
||||
#if self.prevent_aliasing:
|
||||
# obj = image_processing.fourier_upsample(obj)
|
||||
|
||||
exit_waves = self.probe_norm * prs
|
||||
for i in range(self.nz):
|
||||
# If only one object slice
|
||||
if self.obj.dim() == 2:
|
||||
if i == 0 and self.subpixel:
|
||||
# We only need to apply the subpixel shift to the first
|
||||
# slice, because it shifts the probe
|
||||
exit_waves = tools.interactions.ptycho_2D_sinc(
|
||||
exit_waves, obj, pix_trans,
|
||||
shift_probe=True, multiple_modes=True)
|
||||
else:
|
||||
exit_waves = tools.interactions.ptycho_2D_round(
|
||||
exit_waves, obj, pix_trans,
|
||||
multiple_modes=True,upsample_obj=self.prevent_aliasing)
|
||||
|
||||
|
||||
elif self.obj.dim() == 3:
|
||||
# If separate slices
|
||||
if i == 0 and self.subpixel:
|
||||
exit_waves = tools.interactions.ptycho_2D_sinc(
|
||||
exit_waves, obj[i], pix_trans,
|
||||
shift_probe=True, multiple_modes=True)
|
||||
else:
|
||||
exit_waves = tools.interactions.ptycho_2D_round(
|
||||
exit_waves, obj[i], pix_trans,
|
||||
multiple_modes=True, upsample_obj=self.prevent_aliasing)
|
||||
|
||||
#if self.iteration_count >= 1:
|
||||
# plt.imshow(t.abs(tools.propagators.far_field(
|
||||
# exit_waves[0,0].detach()).cpu()))
|
||||
# plt.colorbar()
|
||||
# plt.show()
|
||||
if i < self.nz-1: #on all but the last iteration
|
||||
exit_waves = tools.propagators.near_field(
|
||||
exit_waves,self.as_prop)
|
||||
#plt.imshow(t.abs(exit_waves[0,0].detach().cpu()))
|
||||
#plt.show()
|
||||
|
||||
return exit_waves
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
if self.prevent_aliasing:
|
||||
left = [self.probe.shape[-2]//2,self.probe.shape[-1]//2]
|
||||
right = [self.probe.shape[-2]//2+self.probe.shape[-2],
|
||||
self.probe.shape[-1]//2+self.probe.shape[-1]]
|
||||
|
||||
return tools.propagators.far_field(wavefields)[...,left[0]:right[0],
|
||||
left[1]:right[1]]
|
||||
else:
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
#return tools.losses.poisson_nll(real_data, sim_data, mask=mask,eps=0.5)
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(Multislice2DPtycho, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.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)
|
||||
if self.probe_support is not None:
|
||||
self.probe_support = self.probe_support.to(*args,**kwargs)
|
||||
|
||||
|
||||
self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
self.as_prop = self.as_prop.to(*args, **kwargs)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def corrected_translations(self,dataset):
|
||||
translations = dataset.translations.to(dtype=self.probe.real.dtype,device=self.probe.device)
|
||||
t_offset = tools.interactions.pixel_to_translations(self.probe_basis,self.translation_offsets*self.translation_scale,surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
|
||||
|
||||
def get_rhos(self):
|
||||
# If this is the general unified mode model
|
||||
if self.weights.dim() >= 2:
|
||||
Ws = self.weights.detach().cpu().numpy()
|
||||
rhos_out = np.matmul(np.swapaxes(Ws,1,2), Ws.conj())
|
||||
return rhos_out
|
||||
# This is the purely incoherent case
|
||||
else:
|
||||
return np.array([np.eye(self.probe.shape[0])]*self.weights.shape[0],
|
||||
dtype=np.complex64)
|
||||
|
||||
|
||||
def tidy_probes(self, normalization=1, normalize=False):
|
||||
"""Tidies up the probes
|
||||
|
||||
What we want to do here is use all the information on all the probes
|
||||
to calculate a natural basis for the experiment, and update all the
|
||||
density matrices to operate in that updated basis
|
||||
|
||||
"""
|
||||
|
||||
# First we treat the purely incoherent case
|
||||
|
||||
# I don't love this pattern of using an if statement with a return
|
||||
# to catch this case, but because it's so much simpler than the
|
||||
# unified mode case I think it's appropriate
|
||||
if self.weights.dim() == 1:
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes = analysis.orthogonalize_probes(probe)
|
||||
self.probe.data = t.as_tensor(ortho_probes,
|
||||
device=self.probe.device,dtype=self.probe.dtype)
|
||||
return
|
||||
|
||||
# This is for the unified mode case
|
||||
|
||||
# Note to future: We could probably do this more cleanly with an
|
||||
# SVD directly on the Ws matrix, instead of an eigendecomposition
|
||||
# of the rho matrix.
|
||||
|
||||
rhos = self.get_rhos()
|
||||
overall_rho = np.mean(rhos,axis=0)
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes, A = analysis.orthogonalize_probes(probe,
|
||||
density_matrix=overall_rho,
|
||||
keep_transform=True,
|
||||
normalize=normalize)
|
||||
Aconj = A.conj()
|
||||
Atrans = np.transpose(A)
|
||||
new_rhos = np.matmul(Atrans,np.matmul(rhos,Aconj))
|
||||
|
||||
new_rhos /= normalization
|
||||
ortho_probes *= np.sqrt(normalization)
|
||||
|
||||
dm_rank = self.weights.shape[1]
|
||||
|
||||
new_Ws = []
|
||||
for rho in new_rhos:
|
||||
# These are returned from smallest to largest - we want to keep
|
||||
# the largest ones
|
||||
w,v = sla.eigh(rho)
|
||||
w = w[::-1][:dm_rank]
|
||||
v = v[:,::-1][:,:dm_rank]
|
||||
# For situations where the rank of the density matrix is not
|
||||
# full in reality, but we keep more modes around than needed,
|
||||
# some ws can go negative due to numerical error! This is
|
||||
# extremely rare, but comon enough to cause crashes occasionally
|
||||
# when there are thousands of individual matrices to transform
|
||||
# every time this is called.
|
||||
w = np.maximum(w,0)
|
||||
|
||||
new_Ws.append(np.dot(np.diag(np.sqrt(w)),v.transpose()))
|
||||
|
||||
new_Ws = np.array(new_Ws)
|
||||
|
||||
self.weights.data = t.as_tensor(new_Ws,
|
||||
dtype=self.weights.dtype,device=self.weights.device)
|
||||
|
||||
self.probe.data = t.as_tensor(ortho_probes,
|
||||
device=self.probe.device,dtype=self.probe.dtype)
|
||||
|
||||
|
||||
# Needs to be updated to allow for plotting to an existing figure
|
||||
plot_list = [
|
||||
('Probe Fourier Space Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe if self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
('Probe Fourier Space Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe if self.fourier_probe else tools.propagators.inverse_far_field(self.probe), fig=fig)),
|
||||
('Probe Real Space Amplitude',
|
||||
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)),
|
||||
('Probe Real Space Phase',
|
||||
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: 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: len(self.weights.shape) >=2),
|
||||
('Slice by Slice Real Part of T',
|
||||
lambda self, fig: p.plot_real(self.obj.detach().cpu(), fig=fig, basis=self.probe_basis, units=self.units, cmap='cividis'),
|
||||
lambda self: self.exponentiate_obj),
|
||||
('Slice by Slice Imaginary Part of T',
|
||||
lambda self, fig: p.plot_imag(self.obj.detach().cpu(), fig=fig, basis=self.probe_basis, units=self.units),
|
||||
lambda self: self.exponentiate_obj),
|
||||
('Integrated Real Part of T',
|
||||
lambda self, fig: p.plot_real(t.sum(self.obj.detach().cpu(),dim=0), fig=fig, basis=self.probe_basis, units=self.units, cmap='cividis'),
|
||||
lambda self: (self.exponentiate_obj) and self.obj.dim() >= 3),
|
||||
('Integrated Imaginary Part of T',
|
||||
lambda self, fig: p.plot_imag(t.sum(self.obj.detach().cpu(),dim=0), fig=fig, basis=self.probe_basis, units=self.units),
|
||||
lambda self: (self.exponentiate_obj) and self.obj.dim() >= 3),
|
||||
('Slice by Slice Amplitude of Object Function',
|
||||
lambda self, fig: p.plot_amplitude(self.obj.detach().cpu(), fig=fig, basis=self.probe_basis, units=self.units),
|
||||
lambda self: not self.exponentiate_obj),
|
||||
('Slice by Slice Phase of Object Function',
|
||||
lambda self, fig: p.plot_phase(self.obj.detach().cpu(), fig=fig, basis=self.probe_basis, units=self.units,cmap='cividis'),
|
||||
lambda self: not self.exponentiate_obj),
|
||||
('Amplitude of Stacked Object Function',
|
||||
lambda self, fig: p.plot_amplitude(reduce(t.mul, self.obj.detach().cpu()), fig=fig, basis=self.probe_basis, units=self.units),
|
||||
lambda self: (not self.exponentiate_obj) and self.obj.dim() >=3),
|
||||
('Phase of Stacked Object Function',
|
||||
lambda self, fig: p.plot_phase(reduce(t.mul, self.obj.detach().cpu()), fig=fig, basis=self.probe_basis, units=self.units, cmap='cividis'),
|
||||
lambda self: (not self.exponentiate_obj) and self.obj.dim() >= 3),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig, units=self.units)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
if self.fourier_probe:
|
||||
probe = tools.propagators.inverse_far_field(self.probe)
|
||||
else:
|
||||
probe = self.probe
|
||||
|
||||
probe = probe.detach().cpu().numpy()
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
dz = self.dz
|
||||
nz = self.nz
|
||||
prop = self.as_prop
|
||||
|
||||
return {'basis':basis, 'translation':translations,
|
||||
'probe':probe,'obj':obj,
|
||||
'background':background,
|
||||
'weights':weights, 'dz':dz, 'nz':nz,
|
||||
'interlayer propagator': prop}
|
||||
@@ -0,0 +1,556 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel, FancyPtycho
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
# from CDTools.tools import polarized_plotting as pp
|
||||
from CDTools.tools import analysis
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from scipy import linalg as sla
|
||||
from copy import copy
|
||||
from CDTools.tools import polarization
|
||||
|
||||
__all__ = ['PolarizedFancyPtycho']
|
||||
|
||||
class PolarizedFancyPtycho(FancyPtycho):
|
||||
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis,
|
||||
probe_guess, obj_guess, polarizer, analyzer,
|
||||
detector_slice=None,
|
||||
surface_normal=np.array([0.,0.,1.]),
|
||||
min_translation = t.Tensor([0,0]),
|
||||
background = None, translation_offsets=None,
|
||||
polarizer_offsets=None, analyzer_offsets=None,
|
||||
polarizer_scale=1, analyzer_scale=1, mask=None,
|
||||
weights = None, translation_scale = 1, saturation=None,
|
||||
probe_support = None, oversampling=1,
|
||||
loss='amplitude mse',units='um'):
|
||||
|
||||
super(PolarizedFancyPtycho, self).__init__(wavelength, detector_geometry,
|
||||
probe_basis,
|
||||
probe_guess, obj_guess,
|
||||
detector_slice=None,
|
||||
surface_normal=np.array([0.,0.,1.]),
|
||||
min_translation = t.Tensor([0,0]),
|
||||
background = None, translation_offsets=None, mask=None,
|
||||
weights = weights, translation_scale = 1, saturation=None,
|
||||
probe_support = None, oversampling=1,
|
||||
loss='amplitude mse',units='um')
|
||||
|
||||
if polarizer_offsets is None:
|
||||
self.polarizer_offsets = None
|
||||
else:
|
||||
self.polarizer_offsets = t.nn.Parameter(t.tensor(polarizer_offsets).to(dtype=t.float32)) / polarizer_scale
|
||||
|
||||
if analyzer_offsets is None:
|
||||
self.analyzer_offsets = None
|
||||
else:
|
||||
self.analyzer_offsets = t.nn.Parameter(t.tensor(analyzer_offsets).to(dtype=t.float32)) / analyzer_scale
|
||||
|
||||
self.polarizer = polarizer
|
||||
self.analyzer = analyzer
|
||||
probe_guess = t.tensor(probe_guess, dtype=t.complex64)
|
||||
if probe_guess.dim() > 4:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess[0]))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe_size=None, randomize_ang=0, padding=0, n_modes=1, dm_rank=None, translation_scale = 1, saturation=None, probe_support_radius=None, propagation_distance=None, restrict_obj=-1, scattering_mode=None, oversampling=1, auto_center=False, opt_for_fft=False, loss='amplitude mse', units='um', left_polarized=True):
|
||||
|
||||
# When using this method, remember to pass through the inputs
|
||||
model = FancyPtycho.from_dataset(
|
||||
dataset,
|
||||
probe_size=probe_size,
|
||||
randomize_ang=randomize_ang,
|
||||
padding=padding,
|
||||
n_modes=n_modes,
|
||||
dm_rank=dm_rank,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
probe_support_radius=probe_support_radius,
|
||||
propagation_distance=propagation_distance,
|
||||
scattering_mode=scattering_mode,
|
||||
oversampling=oversampling,
|
||||
auto_center=auto_center,
|
||||
opt_for_fft=opt_for_fft,
|
||||
loss=loss,
|
||||
units=units)
|
||||
|
||||
|
||||
# Mutate the class to its subclass
|
||||
model.__class__ = cls
|
||||
|
||||
if left_polarized:
|
||||
x = 1j
|
||||
else:
|
||||
x = -1j
|
||||
|
||||
probe = model.probe.detach()
|
||||
probe = t.cat((probe, probe * x), dim=-3)
|
||||
probe_max = t.max(t.abs(probe))
|
||||
probe_stack = [0.01 * probe_max * t.rand(probe.shape, dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
probe = t.stack([probe, ] + probe_stack)
|
||||
#print('probe', type(probe), probe.shape)
|
||||
model.probe.data = probe
|
||||
#print(model.probe.shape)
|
||||
# obj = t.stack((model.obj.data, model.obj.data), dim=-3)
|
||||
# model.obj.data = t.stack((obj.data, obj.data), dim=-4)
|
||||
# obj = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
obj = model.obj.detach()
|
||||
# Abe - Probably something identity matrix-like would be a better
|
||||
# initialization (e.g. ((obj,0*obj),(0*obj,obj))
|
||||
obj = t.stack((obj, obj), dim=-3)
|
||||
obj = t.stack((obj, obj), dim=-4)
|
||||
#print('object', type(obj), obj.shape)
|
||||
model.obj.data = obj
|
||||
#print('polarized fancy ptycho from datset obj')
|
||||
a = obj.detach()
|
||||
#plt.imshow(np.real(a[0, 0, :, :]))
|
||||
#plt.figure()
|
||||
#plt.imshow(np.real(a[0, 1, :, :]))
|
||||
#plt.show()
|
||||
|
||||
# tensor vs tensor.data
|
||||
return model
|
||||
|
||||
polarizers = [tools.polarization.generate_linear_polarizer(i * 45) for i in range(3)]
|
||||
|
||||
@classmethod
|
||||
def from_dataset2(cls, dataset, probe_size=None, randomize_ang=0, padding=0, n_modes=1, dm_rank=None, translation_scale=1, saturation=None, probe_support_radius=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=False, opt_for_fft=False, loss='amplitude mse', units='um'):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
|
||||
# We include the *extras to make this work even with datasets, like
|
||||
# polarization dependent datasets, that might toss out extra inputs
|
||||
(indices, translations, polarizer, analyzer), patterns = dataset[:]
|
||||
|
||||
dataset.get_as(*get_as_args[0], **get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns, dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
if left_polarized:
|
||||
x = 1j
|
||||
else:
|
||||
x = -1j
|
||||
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
probe_shape = t.stack((2, probe_shape), dim=-3)
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:, 0], det_basis[:, 1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0., 0., 1.])
|
||||
surface_normal /= -np.linalg.norm(surface_normal)
|
||||
|
||||
# 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, padding=200)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
if probe_size is None:
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
else:
|
||||
probe = tools.initializers.gaussian_probe(dataset, probe_basis, probe_shape, probe_size, propagation_distance=propagation_distance)
|
||||
|
||||
# Now we initialize all the subdominant probe modes
|
||||
probe_max = t.max(t.abs(probe))
|
||||
probe_stack = [0.01 * probe_max * t.rand(probe.shape, dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
probe = t.stack([probe, ] + probe_stack)
|
||||
# probe = t.stack([tools.propagators.far_field(probe),] + probe_stack)
|
||||
probe_x, probe_y = probe, probe * x
|
||||
probe = t.stact((probe_x, probe_y), dim=-3)
|
||||
|
||||
a = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
b = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
c = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
d = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
|
||||
ab = t.stack((a, b), dim=-3)
|
||||
cd = t.stack((c, d), dim=-3)
|
||||
obj = t.stack((ab, cd), dim=-4)
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset), 2)) - 0.5)
|
||||
|
||||
if dm_rank is not None and dm_rank != 0:
|
||||
if dm_rank > n_modes:
|
||||
raise KeyError('Density matrix rank cannot be greater than the number of modes. Use dm_rank = -1 to use a full rank matrix.')
|
||||
elif dm_rank == -1:
|
||||
# dm_rank == -1 is defined to mean full-rank
|
||||
dm_rank = n_modes
|
||||
|
||||
Ws = t.zeros(len(dataset), dm_rank, n_modes, dtype=t.complex64)
|
||||
# Start with as close to the identity matrix as possible,
|
||||
# cutting of when we hit the specified maximum rank
|
||||
for i in range(0, dm_rank):
|
||||
Ws[:, i, i] = 1
|
||||
else:
|
||||
# dm_rank == None or dm_rank = 0 triggers a special case where
|
||||
# a standard incoherent multi-mode model is used. This is the
|
||||
# default, because it is so common.
|
||||
# In this case, we define a set of weights which only has one index
|
||||
Ws = t.ones(len(dataset))
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
if probe_support_radius is not None:
|
||||
probe_support = t.zeros(probe[0].shape, dtype=t.bool)
|
||||
xs, ys = np.mgrid[:probe.shape[-2], :probe.shape[-1]]
|
||||
xs = xs - np.mean(xs)
|
||||
ys = ys - np.mean(ys)
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
probe_support[Rs < probe_support_radius] = 1
|
||||
probe = probe * probe_support[None, :, :]
|
||||
|
||||
else:
|
||||
probe_support = None
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, probe, obj,
|
||||
detector_slice=det_slice,
|
||||
surface_normal=surface_normal,
|
||||
min_translation=min_translation,
|
||||
translation_offsets=translation_offsets,
|
||||
weights=Ws, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
probe_support=probe_support,
|
||||
oversampling=oversampling,
|
||||
loss=loss, units=units)
|
||||
|
||||
|
||||
|
||||
def interaction(self, index, translations, polarizer, analyzer, test=False):
|
||||
|
||||
# Step 1 is to convert the translations for each position into a
|
||||
# value in pixels
|
||||
pix_trans = tools.interactions.translations_to_pixel(self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans -= self.min_translation
|
||||
# We then add on any recovered translation offset, if they exist
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += self.translation_scale * self.translation_offsets[index]
|
||||
|
||||
|
||||
# This restricts the basis probes to stay within the probe support
|
||||
basis_prs = self.probe * self.probe_support[...,:,:] # This makes no sense
|
||||
# self.probe is an Nx2xXxY stach of probes
|
||||
|
||||
# Now we construct the probes for each shot from the basis probes
|
||||
Ws = self.weights[index]
|
||||
if len(self.weights[0].shape) == 0:
|
||||
# If a purely stable coherent illumination is defined
|
||||
# Ws is a tensor of length M, M is the number of frames to be processed
|
||||
prs = Ws[...,None,None,None,None] * basis_prs
|
||||
else:
|
||||
raise NotImplementedError('Unstable Modes not Implemented for polarized light')
|
||||
|
||||
pol_probes = polarization.apply_linear_polarizer(prs, polarizer)
|
||||
exit_waves = self.probe_norm * tools.interactions.ptycho_2D_sinc(
|
||||
pol_probes, self.obj, pix_trans,
|
||||
shift_probe=True, multiple_modes=True, polarized=True)
|
||||
# We're losing some efficiency here, because we only need to keep
|
||||
# around the scalar wavefield after analyzing the waves.
|
||||
# But I think it's not a huge issue - Abe
|
||||
analyzed_exit_waves = polarization.apply_linear_polarizer(exit_waves, analyzer)
|
||||
return analyzed_exit_waves
|
||||
|
||||
|
||||
def vectorial_wavefields(wavefields, func, *args, **kwargs):
|
||||
wavefields_x = wavefields[..., 0, :, :, :]
|
||||
wavefields_y = wavefields[..., 1, :, :, :]
|
||||
out_x = func(wavefields_x, *args, **kwargs)
|
||||
out_y = func(wavefields_y, *args, **kwargs)
|
||||
out = t.stack((out_x, out_y), dim=-4)
|
||||
return out[..., None, :, :]
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
wavefields_x = wavefields[..., 0, :, :]
|
||||
wavefields_y = wavefields[..., 1, :, :]
|
||||
out_x = tools.measurements.quadratic_background(wavefields_x,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
# now, set bckgr to 0 since t shouldn't be calculated twice
|
||||
out_y = tools.measurements.quadratic_background(wavefields_y,
|
||||
0,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
return out_x + out_y
|
||||
|
||||
|
||||
# Note: No "loss" function is defined here, because it is added
|
||||
# dynamically during object creation in __init__
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(PolarizedFancyPtycho, self).to(*args, **kwargs)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def corrected_translations(self, dataset):
|
||||
translations = dataset.translations.to(dtype=t.float32,device=self.probe.device)
|
||||
t_offset = tools.interactions.pixel_to_translations(self.probe_basis,self.translation_offsets*self.translation_scale,surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
|
||||
|
||||
|
||||
def get_rhos(self):
|
||||
# If this is the general unified mode model
|
||||
if self.weights.dim() >= 2:
|
||||
Ws = self.weights.detach().cpu().numpy()
|
||||
rhos_out = np.matmul(np.swapaxes(Ws,1,2), Ws.conj())
|
||||
return rhos_out
|
||||
# This is the purely incoherent case
|
||||
else:
|
||||
return np.array([np.eye(self.probe.shape[0])]*self.weights.shape[0],
|
||||
dtype=np.complex64)
|
||||
|
||||
def tidy_probes(self, normalization=1, normalize=False):
|
||||
"""Tidies up the probes
|
||||
|
||||
What we want to do here is use all the information on all the probes
|
||||
to calculate a natural basis for the experiment, and update all the
|
||||
density matrices to operate in that updated basis
|
||||
|
||||
"""
|
||||
|
||||
# First we treat the purely incoherent case
|
||||
|
||||
# I don't love this pattern of using an if statement with a return
|
||||
# to catch this case, but because it's so much simpler than the
|
||||
# unified mode case I think it's appropriate
|
||||
if self.weights.dim() == 1:
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes = analysis.orthogonalize_probes(probe)
|
||||
self.probe.data = t.as_tensor(ortho_probes,
|
||||
device=self.probe.device,dtype=self.probe.dtype)
|
||||
return
|
||||
|
||||
# This is for the unified mode case
|
||||
|
||||
# Note to future: We could probably do this more cleanly with an
|
||||
# SVD directly on the Ws matrix, instead of an eigendecomposition
|
||||
# of the rho matrix.
|
||||
|
||||
rhos = self.get_rhos()
|
||||
overall_rho = np.mean(rhos,axis=0)
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
ortho_probes, A = analysis.orthogonalize_probes(probe,
|
||||
density_matrix=overall_rho,
|
||||
keep_transform=True,
|
||||
normalize=normalize)
|
||||
Aconj = A.conj()
|
||||
Atrans = np.transpose(A)
|
||||
new_rhos = np.matmul(Atrans,np.matmul(rhos,Aconj))
|
||||
|
||||
new_rhos /= normalization
|
||||
ortho_probes *= np.sqrt(normalization)
|
||||
|
||||
dm_rank = self.weights.shape[1]
|
||||
|
||||
new_Ws = []
|
||||
for rho in new_rhos:
|
||||
# These are returned from smallest to largest - we want to keep
|
||||
# the largest ones
|
||||
w,v = sla.eigh(rho)
|
||||
w = w[::-1][:dm_rank]
|
||||
v = v[:,::-1][:,:dm_rank]
|
||||
# For situations where the rank of the density matrix is not
|
||||
# full in reality, but we keep more modes around than needed,
|
||||
# some ws can go negative due to numerical error! This is
|
||||
# extremely rare, but comon enough to cause crashes occasionally
|
||||
# when there are thousands of individual matrices to transform
|
||||
# every time this is called.
|
||||
w = np.maximum(w,0)
|
||||
|
||||
new_Ws.append(np.dot(np.diag(np.sqrt(w)),v.transpose()))
|
||||
|
||||
new_Ws = np.array(new_Ws)
|
||||
|
||||
self.weights.data = t.as_tensor(new_Ws,
|
||||
dtype=self.weights.dtype,device=self.weights.device)
|
||||
|
||||
self.probe.data = t.as_tensor(ortho_probes,
|
||||
device=self.probe.device,dtype=self.probe.dtype)
|
||||
|
||||
|
||||
def plot_wavefront_variation(self, dataset,fig=None,mode='amplitude',**kwargs):
|
||||
def get_probes(idx):
|
||||
basis_prs = self.probe * self.probe_support[...,:,:]
|
||||
prs = t.sum(self.weights[idx,:,:,None,None] * basis_prs, axis=-4)
|
||||
ortho_probes = analysis.orthogonalize_probes(prs)
|
||||
|
||||
if mode.lower() == 'amplitude':
|
||||
return np.abs(ortho_probes.detach().cpu().numpy())
|
||||
if mode.lower() == 'root_sum_intensity':
|
||||
return np.sum(np.abs(ortho_probes.detach().cpu().numpy())**2,axis=0)
|
||||
if mode.lower() == 'phase':
|
||||
return np.angle(ortho_probes.detach().cpu().numpy())
|
||||
|
||||
probe_matrix = np.zeros([self.probe.shape[0]]*2,
|
||||
dtype=np.complex64)
|
||||
np_probes = self.probe.detach().cpu().numpy()
|
||||
for i in range(probe_matrix.shape[0]):
|
||||
for j in range(probe_matrix.shape[0]):
|
||||
probe_matrix[i,j] = np.sum(np_probes[i]*np_probes[j].conj())
|
||||
|
||||
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
|
||||
probe_intensities = np.sum(np.tensordot(weights,probe_matrix,axes=1)*
|
||||
weights.conj(),axis=2)
|
||||
|
||||
# Imaginary part is already essentially zero up to rounding error
|
||||
probe_intensities = np.real(probe_intensities)
|
||||
|
||||
values = np.sum(probe_intensities,axis=1)
|
||||
if mode.lower() == 'amplitude' or mode.lower() == 'root_sum_intensity':
|
||||
cmap = 'viridis'
|
||||
else:
|
||||
cmap = 'twilight'
|
||||
|
||||
p.plot_nanomap_with_images(self.corrected_translations(dataset), get_probes, values=values, fig=fig, units=self.units, basis=self.probe_basis, nanomap_colorbar_title='Total Probe Intensity',cmap=cmap,**kwargs),
|
||||
|
||||
|
||||
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: 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: 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: len(self.weights.shape) >= 2),
|
||||
('Basis Probe Amplitudes (scroll to view modes)',
|
||||
lambda self, fig: p.plot_amplitude(self.probe, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Basis Probe Phases (scroll to view modes)',
|
||||
lambda self, fig: p.plot_phase(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: 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: len(self.weights.shape) >= 2),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, 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)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig, units=self.units)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
|
||||
return {'basis':basis, 'translation':translations,
|
||||
'probe':probe,'obj':obj,
|
||||
'background':background,
|
||||
'weights':weights}
|
||||
@@ -0,0 +1,437 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools.interactions import RPI_interaction
|
||||
from CDTools.tools import initializers
|
||||
from scipy.ndimage.morphology import binary_dilation
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
import time
|
||||
|
||||
__all__ = ['RPI']
|
||||
|
||||
#
|
||||
# This model has to work a bit differently from a ptychography model
|
||||
# because a typical RPI dataset will have lots of images, each of which
|
||||
# can be reconstructed on it's own. I can see a few ways to approach this:
|
||||
#
|
||||
# 1) Enfore 1 image per dataset as a restriction for internal consistency
|
||||
# of the "model" idea
|
||||
#
|
||||
# 2) Override some of the base functions of the CDIModel to accept optional
|
||||
# parameters that make it work on larger datasets
|
||||
#
|
||||
# I think #1 is basically untenable, because it would require creating
|
||||
# a new dataset and model for each reconstruction - which means instantiating
|
||||
# tons of stuff and storing all sorts of excess information for each frame,
|
||||
# when that could easily be reused for other frames. As a result, I think I
|
||||
# will try the following pattern:
|
||||
#
|
||||
# 1) A model will contain a single object "guess" at all times
|
||||
# 2) Constructing a model from a data will automatically instantiate the object guess from the first diffraction pattern in the dataset
|
||||
# 3) All the optimization functions will get an additional argument for the index of the diffraction pattern to reconstruct. This could either be handled by editing the CDIModel base class to pass through some kwargs, or by overriding all the optimization functions explicitly.
|
||||
# 4) A few convenience functions can be written to re-initialize the object array from any image / index in the dataset
|
||||
# 5) A new function can be written to reconstruct the entire dataset by running through each pattern one at a time.
|
||||
#
|
||||
# The advantage of this approach is that I can start by writing a class that
|
||||
# will only reconstruct the first diffraction pattern from a dataset and then
|
||||
# extend it.
|
||||
#
|
||||
# Final note: It is worth seeing whether it is possible to include the
|
||||
# probe propagation explicitly as a parameter which can be reconstructed
|
||||
# via gradient descent.
|
||||
#
|
||||
#
|
||||
|
||||
__all__ = ['RPI']
|
||||
|
||||
class RPI(CDIModel):
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
return t.complex(self.obj_real, self.obj_imag)
|
||||
|
||||
@property
|
||||
def weights(self):
|
||||
ws = t.complex(self.weights_real, self.weights_imag)
|
||||
return ws / 10# / self.obj_real.size().numel()
|
||||
|
||||
|
||||
|
||||
def __init__(self, wavelength, detector_geometry, probe_basis,
|
||||
probe, obj_guess, detector_slice=None,
|
||||
background=None, mask=None, saturation=None,
|
||||
obj_support=None, oversampling=1, weight_matrix=False):
|
||||
|
||||
super(RPI, self).__init__()
|
||||
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
|
||||
scale_factor = t.tensor([probe.shape[-1]/obj_guess.shape[-1],
|
||||
probe.shape[-2]/obj_guess.shape[-2]])
|
||||
self.obj_basis = self.probe_basis * scale_factor
|
||||
self.detector_slice = detector_slice
|
||||
|
||||
# Maybe something to include in a bit
|
||||
# self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.mask = t.tensor(mask, dtype=t.bool)
|
||||
|
||||
|
||||
self.probe = t.tensor(probe, dtype=t.complex64)
|
||||
|
||||
if obj_guess.dim() == 2:
|
||||
obj_guess = obj_guess[None, :, :]
|
||||
|
||||
obj_guess = t.tensor(obj_guess, dtype=t.complex64)
|
||||
|
||||
self.obj_real = t.nn.Parameter(obj_guess.real)
|
||||
self.obj_imag = t.nn.Parameter(obj_guess.imag)
|
||||
|
||||
self.weights_real = t.nn.Parameter(t.eye(probe.shape[0])* 10)# * self.obj_real.size().numel())
|
||||
self.weights_imag = t.nn.Parameter(t.zeros(probe.shape[0]))
|
||||
|
||||
if not weight_matrix:
|
||||
self.weights_real.requires_grad=False
|
||||
self.weights_imag.requires_grad=False
|
||||
|
||||
# Wait for LBFGS to be updated for complex-valued parameters
|
||||
# self.obj = t.nn.Parameter(obj_guess.to(t.float32))
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[0][self.detector_slice].shape,
|
||||
dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.tensor(background, dtype=t.float32)
|
||||
|
||||
if obj_support is not None:
|
||||
self.obj_support = obj_support
|
||||
self.obj.data = self.obj * obj_support[None, ...]
|
||||
else:
|
||||
self.obj_support = t.ones_like(self.obj[0, ...])
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe, obj_size=None, background=None, mask=None, padding=0, n_modes=1, saturation=None, scattering_mode=None, oversampling=1, auto_center=False, initialization='random', opt_for_fft=False, weight_matrix=False, probe_threshold=0):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
# We only need the patterns here, not the inputs associated with them.
|
||||
_, patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns,dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
if not isinstance(probe,t.Tensor):
|
||||
probe = t.as_tensor(probe)
|
||||
|
||||
# Potentially need all of this orientation stuff later
|
||||
|
||||
#if hasattr(dataset, 'sample_info') and \
|
||||
# dataset.sample_info is not None and \
|
||||
# 'orientation' in dataset.sample_info:
|
||||
# surface_normal = dataset.sample_info['orientation'][2]
|
||||
#else:
|
||||
# surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
#if scattering_mode in {'t', 'transmission'}:
|
||||
# surface_normal = np.array([0.,0.,1.])
|
||||
#elif scattering_mode in {'r', 'reflection'}:
|
||||
# outgoing_dir = np.cross(det_basis[:,0], det_basis[:,1])
|
||||
# outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
# surface_normal = outgoing_dir + np.array([0.,0.,1.])
|
||||
# surface_normal /= np.linalg.norm(surface_normal)
|
||||
|
||||
|
||||
if background is None and hasattr(dataset, 'background') \
|
||||
and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
elif background is not None:
|
||||
background = t.sqrt(t.Tensor(background).to(dtype=t.float32))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
# If no mask is given, but one exists in the dataset, load it.
|
||||
if mask is None and hasattr(dataset, 'mask') \
|
||||
and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
|
||||
# Now we initialize the object
|
||||
if obj_size is None:
|
||||
# This is a standard size for a well-matched probe and detector
|
||||
obj_size = (np.array(probe_shape) // 2).astype(int)
|
||||
|
||||
if initialization.lower().strip() == 'random':
|
||||
# I think something to do with the fact that the object is defined
|
||||
# on a coarser grid needs to be accounted for here that is not
|
||||
# accounted for yet
|
||||
scale = t.sum(patterns[0]) / t.sum(t.abs(probe)**2)
|
||||
obj_guess = scale * t.exp(2j * np.pi * t.rand([n_modes,]+obj_size))
|
||||
elif initialization.lower().strip() == 'spectral':
|
||||
if background is not None:
|
||||
obj_guess = initializers.RPI_spectral_init(
|
||||
patterns[0], probe, obj_size, mask=mask,
|
||||
background=background**2, n_modes=n_modes)
|
||||
else:
|
||||
obj_guess = initializers.RPI_spectral_init(
|
||||
patterns[0], probe, obj_size, mask=mask,
|
||||
n_modes=n_modes)
|
||||
|
||||
else:
|
||||
raise KeyError('Initialization "' + str(initialization) + \
|
||||
'" invalid - use "spectral" or "random"')
|
||||
|
||||
probe_intensity = t.sqrt(t.sum(t.abs(probe)**2,axis=0))
|
||||
probe_fft = tools.propagators.far_field(probe_intensity)
|
||||
pad0l = (probe.shape[-2] - obj_size[-2])//2
|
||||
pad0r = probe.shape[-2] - obj_size[-2] - pad0l
|
||||
pad1l = (probe.shape[-1] - obj_size[-1])//2
|
||||
pad1r = probe.shape[-1] - obj_size[-1] - pad1l
|
||||
probe_lr_fft = probe_fft[pad0l:-pad0r,pad1l:-pad1r]
|
||||
probe_lr = t.abs(tools.propagators.inverse_far_field(probe_lr_fft))
|
||||
|
||||
obj_support = probe_lr > t.max(probe_lr) * probe_threshold
|
||||
obj_support = t.as_tensor(binary_dilation(obj_support))
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis,
|
||||
probe, obj_guess, detector_slice=det_slice,
|
||||
background=background, mask=mask, saturation=saturation,
|
||||
obj_support=obj_support, oversampling=oversampling,
|
||||
weight_matrix=weight_matrix)
|
||||
|
||||
@classmethod
|
||||
def from_calibration(cls, calibration, obj_size=None, n_modes=1, saturation=None):
|
||||
|
||||
wavelength = calibration['wavelength']
|
||||
probe_basis = t.as_tensor(calibration['basis'])
|
||||
probe = t.as_tensor(calibration['probe'])
|
||||
if 'background' in calibration:
|
||||
background = t.as_tensor(calibration['background'])
|
||||
else:
|
||||
background = t.zeros_like(probe.real)
|
||||
if 'mask' in calibration:
|
||||
mask = t.as_tensor(calibration['mask'])
|
||||
else:
|
||||
mask = t.ones_like(probe.real, dtype=t.bool)
|
||||
|
||||
# Now we initialize the object
|
||||
if obj_size is None:
|
||||
# This is a standard size for a well-matched probe and detector
|
||||
obj_size = (np.array(probe.shape[-2:]) // 2).astype(int)
|
||||
|
||||
obj_guess = t.exp(2j * np.pi * t.rand([n_modes,]+obj_size))
|
||||
|
||||
det_geo = {'distance': 1,
|
||||
'basis': wavelength / probe_basis} # Is this even right?
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis,
|
||||
probe, obj_guess,
|
||||
background=background, mask=mask)
|
||||
|
||||
|
||||
def random_init(self, pattern):
|
||||
scale = t.sum(pattern) / t.sum(t.abs(self.probe)**2)
|
||||
self.obj.data = scale * t.exp(
|
||||
2j * np.pi * t.rand(self.obj.shape)).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
|
||||
def spectral_init(self, pattern):
|
||||
if self.background is not None:
|
||||
self.obj.data = initializers.RPI_spectral_init(
|
||||
pattern, self.probe, self.obj.shape[-3:-1], mask=self.mask,
|
||||
background=self.background**2, n_modes=self.obj.shape[0]).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
else:
|
||||
self.obj.data = initializers.RPI_spectral_init(
|
||||
pattern, self.probe, self.obj.shape[-3:-1], mask=self.mask,
|
||||
n_modes=self.obj.shape[0]).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
|
||||
# Needs work
|
||||
def interaction(self, index, *args):
|
||||
# including *args allows this to work with all sorts of datasets
|
||||
# that might include other information in with the index in their
|
||||
# "input" parameters (such as translations for a ptychography dataset).
|
||||
# This makes it seamless to use such a dataset even though those
|
||||
# extra arguments will not be used.
|
||||
|
||||
|
||||
all_exit_waves = []
|
||||
|
||||
# Mix the probes with the weight matrix
|
||||
prs = t.sum(self.weights[..., None, None] * self.probe, axis=-3)
|
||||
|
||||
for i in range(self.probe.shape[0]):
|
||||
pr = prs[i]
|
||||
# Here we have a 3D probe (one single mode)
|
||||
# and a 4D object (multiple modes mixing incoherently)
|
||||
exit_waves = RPI_interaction(pr,
|
||||
self.obj_support * self.obj)
|
||||
|
||||
all_exit_waves.append(exit_waves)
|
||||
|
||||
# This creates a bunch of modes generated from all possible combos
|
||||
# of the probe and object modes all strung out along the first index
|
||||
output = t.cat(all_exit_waves)
|
||||
|
||||
# If we have multiple indexes input, we unsqueeze and repeat the stack
|
||||
# of wavefields enough times to simulate each requested index. This
|
||||
# seems silly, but it enables (for example) one to do a reconstruction
|
||||
# from a set of diffraction patterns that are all known to be from the
|
||||
# same object.
|
||||
try:
|
||||
# will fail if index has no length, for example when index
|
||||
# is just an int. In this case, we just do nothing instead
|
||||
output = output.unsqueeze(0).repeat(1,len(index),1,1,1)
|
||||
except TypeError:
|
||||
pass
|
||||
return output
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
p = tools.propagators.far_field(wavefields)
|
||||
return p
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
# Here I'm taking advantage of an undocumented feature in the
|
||||
# incoherent_sum measurement function where it will work with
|
||||
# a 4D wavefield array as well as a 5D array.
|
||||
m = tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
return m
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
#return tools.losses.poisson_nll(real_data, sim_data, mask=mask)
|
||||
|
||||
def regularizer(self, factors):
|
||||
if self.obj.shape[0] == 1:
|
||||
return factors[0] * t.sum(t.abs(self.obj[0,:,:])**2)
|
||||
else:
|
||||
return factors[0] * t.sum(t.abs(self.obj[0,:,:])**2) \
|
||||
+ factors[1] * t.sum(t.abs(self.obj[1:,:,:])**2)
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(RPI, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
|
||||
self.probe = self.probe.to(*args,**kwargs)
|
||||
self.probe_basis = self.probe_basis.to(*args,**kwargs)
|
||||
self.obj_basis = self.obj_basis.to(*args,**kwargs)
|
||||
self.obj_support = self.obj_support.to(*args,**kwargs)
|
||||
self.background = self.background.to(*args, **kwargs)
|
||||
|
||||
# Maybe include in a bit
|
||||
#self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
def sim_to_dataset(self, args_list):
|
||||
raise NotImplementedError('No sim to dataset yet, sorry!')
|
||||
|
||||
plot_list = [
|
||||
('Root Sum Squared Amplitude of all Probes',
|
||||
lambda self, fig: p.plot_amplitude(
|
||||
np.sqrt(np.sum((t.abs(t.sum(self.weights[..., None, None].detach() * self.probe, axis=-3))**2).cpu().numpy(),axis=0)),
|
||||
fig=fig, basis=self.probe_basis)),
|
||||
('Dominant Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj[0], fig=fig,
|
||||
basis=self.obj_basis)),
|
||||
('Dominant Object Phase',
|
||||
lambda self, fig: p.plot_phase(self.obj[0], fig=fig,
|
||||
basis=self.obj_basis)),
|
||||
('Subdominant Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj[1], fig=fig,
|
||||
basis=self.obj_basis),
|
||||
lambda self: len(self.obj) >=2),
|
||||
('Subdominant Object Phase',
|
||||
lambda self, fig: p.plot_phase(self.obj[1], fig=fig,
|
||||
basis=self.obj_basis),
|
||||
lambda self: len(self.obj) >=2)
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset=None, full_obj=False):
|
||||
# dataset is set as a kwarg here because it isn't needed, but the
|
||||
# common pattern is to pass a dataset. This makes it okay if one
|
||||
# continues to use that standard pattern
|
||||
probe_basis = self.probe_basis.detach().cpu().numpy()
|
||||
obj_basis = self.obj_basis.detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
# Provide the option to save out the subdominant objects or
|
||||
# just the dominant one
|
||||
if full_obj:
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
else:
|
||||
obj = self.obj[0].detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
|
||||
return {'probe_basis': probe_basis, 'obj_basis': obj_basis,
|
||||
'probe': probe,'obj': obj,
|
||||
'background': background}
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import cmath
|
||||
from CDTools.tools import plotting as p
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
|
||||
__all__ = ['SMatrixPtycho']
|
||||
|
||||
class SMatrixPtycho(CDIModel):
|
||||
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis, probe_guess, probe_fourier_support,
|
||||
s_matrix_guess,
|
||||
detector_slice=None,
|
||||
surface_normal=np.array([0.,0.,1.]),
|
||||
min_translation = t.Tensor([0,0]),
|
||||
background = None, translation_offsets=None,
|
||||
probe_planes = None, mask=None,
|
||||
weights = None, translation_scale = 1, saturation=None,
|
||||
oversampling=1):
|
||||
|
||||
super(SMatrixPtycho,self).__init__()
|
||||
self.wavelength = t.Tensor([wavelength])
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.Tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.Tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.Tensor(det_geo['corner'])
|
||||
|
||||
self.min_translation = t.Tensor(min_translation)
|
||||
|
||||
self.probe_basis = t.Tensor(probe_basis)
|
||||
self.detector_slice = detector_slice
|
||||
self.surface_normal = t.Tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.mask = t.BoolTensor(mask)
|
||||
|
||||
# We rescale the probe here so it learns at the same rate as the
|
||||
# object
|
||||
# Remember that for S-matrix we have several probes for different
|
||||
# planes
|
||||
if probe_guess.dim() > 4:
|
||||
self.probe_norm = 1 * t.max(tools.cmath.cabs(probe_guess[0,0].to(t.float32)))
|
||||
else:
|
||||
self.probe_norm = 1 * t.max(tools.cmath.cabs(probe_guess[0].to(t.float32)))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess.to(t.float32)
|
||||
/ self.probe_norm)
|
||||
|
||||
self.s_matrix = t.nn.Parameter(s_matrix_guess.to(t.float32))
|
||||
|
||||
if background is None:
|
||||
ew_shape = [s_matrix_guess.shape[0] - 1 + probe_guess.shape[-3],
|
||||
s_matrix_guess.shape[1] - 1 + probe_guess.shape[-2]]
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(t.ones(ew_shape)[self.detector_slice].shape).to(t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(ew_shape).to(t.float32)
|
||||
|
||||
|
||||
self.background = t.nn.Parameter(t.Tensor(background).to(t.float32))
|
||||
|
||||
if weights is None:
|
||||
self.weights = None
|
||||
else:
|
||||
self.weights = t.nn.Parameter(t.Tensor(weights).to(t.float32))
|
||||
|
||||
if translation_offsets is None:
|
||||
self.translation_offsets = None
|
||||
else:
|
||||
self.translation_offsets = t.nn.Parameter(t.Tensor(translation_offsets).to(t.float32)/ translation_scale)
|
||||
|
||||
# This maps indices to probe planes to be used. If none, it defaults
|
||||
# to always being plane 0
|
||||
if probe_planes is None:
|
||||
self.probe_planes = None
|
||||
else:
|
||||
self.probe_planes = t.LongTensor(probe_planes)
|
||||
|
||||
self.translation_scale = translation_scale
|
||||
|
||||
self.probe_fourier_support = t.Tensor(probe_fourier_support).to(t.float32)
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe_convergence_radius, locality_radius=1, probe_size=None, randomize_ang=0, padding=0, n_modes=1, translation_scale = 1, saturation=None, propagation_distance=None, scattering_mode=None, oversampling=1):
|
||||
|
||||
datasets = [dataset]
|
||||
propagation_distances = [propagation_distance]
|
||||
|
||||
# We only return the 0th element because in the general case, the
|
||||
# constructor needs to return a stacked datset in addition to
|
||||
# a model, but for the case of one dataset we only need to return
|
||||
# the model.
|
||||
return cls.from_datasets(datasets, probe_convergence_radius,
|
||||
locality_radius=locality_radius,
|
||||
probe_size=probe_size,
|
||||
randomize_ang=randomize_ang,
|
||||
padding=padding,
|
||||
n_modes=n_modes,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
propagation_distances=propagation_distances,
|
||||
scattering_mode=scattering_mode,
|
||||
oversampling=oversampling)[0]
|
||||
|
||||
# This is for the multi-focal-plane case, where each dataset will correspond
|
||||
# to a different focal plane. The guess propagation distance for each
|
||||
# dataset can be set individually but otherwise the probes are
|
||||
# reconstructed entirely separately. All datasets are assumed to have
|
||||
# the same basic parameters (wavelength, detector geometry, etc) and share
|
||||
# the same origin in the x-y plane.
|
||||
@classmethod
|
||||
def from_datasets(cls, datasets, probe_convergence_radius, locality_radius=1, probe_size=None, randomize_ang=0, padding=0, n_modes=1, translation_scale = 1, saturation=None, propagation_distances=None, scattering_mode=None, oversampling=1):
|
||||
|
||||
wavelength = datasets[0].wavelength
|
||||
det_basis = datasets[0].detector_geometry['basis']
|
||||
det_shape = datasets[0][0][1].shape
|
||||
distance = datasets[0].detector_geometry['distance']
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, ew_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
padding=padding,
|
||||
opt_for_fft=False,
|
||||
oversampling=oversampling)
|
||||
|
||||
if propagation_distances is None:
|
||||
propagation_distances = [None] * len(datasets)
|
||||
|
||||
# This shrinks the probe to ensure that the output wavefield
|
||||
# is the correct shape
|
||||
probe_shape = t.Size(np.array(ew_shape) - (2*locality_radius))
|
||||
|
||||
# always do this on the cpu
|
||||
probe_planes = []
|
||||
translations = []
|
||||
patterns = []
|
||||
probes = []
|
||||
for i, dataset in enumerate(datasets):
|
||||
get_as_args = dataset.get_as_args
|
||||
|
||||
dataset.get_as(device='cpu')
|
||||
(indices, tx), pats = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
translations.append(tx)
|
||||
probe_planes.extend([i]*tx.shape[0])
|
||||
patterns.append(pats)
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
if locality_radius != 0:
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, ew_shape, det_slice, propagation_distance=propagation_distances[i], oversampling=oversampling)[locality_radius:-locality_radius,locality_radius:-locality_radius]
|
||||
else:
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, ew_shape, det_slice, propagation_distance=propagation_distances[i], oversampling=oversampling)
|
||||
|
||||
# Now we initialize all the subdominant probe modes
|
||||
probe_max = t.max(cmath.cabs(probe))
|
||||
probe_stack = [0.01 * probe_max * t.rand(probe.shape,dtype=probe.dtype) for i in range(n_modes - 1)]
|
||||
probe = t.stack([tools.propagators.inverse_far_field(probe),] + probe_stack)
|
||||
probes.append(probe)
|
||||
|
||||
translations = t.cat(translations)
|
||||
patterns = t.cat(patterns)
|
||||
probes = t.stack(probes)
|
||||
|
||||
|
||||
if hasattr(datasets[0], 'sample_info') and \
|
||||
datasets[0].sample_info is not None and \
|
||||
'orientation' in datasets[0].sample_info:
|
||||
surface_normal = datasets[0].sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:,0], det_basis[:,1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0.,0.,1.])
|
||||
surface_normal /= np.linalg.norm(outgoing_dir)
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
# The locality radius correction is probably not needed because
|
||||
# it will always be way less than 200, but it ensures that there
|
||||
# is no wrapping in the s-matrix
|
||||
obj_size, min_translation = tools.initializers.calc_object_setup(probe_shape, pix_translations, padding=200+2*locality_radius)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(datasets[0].background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
|
||||
|
||||
s_matrix = t.zeros([2*locality_radius+1,2*locality_radius+1,obj_size[0],
|
||||
obj_size[1],2])
|
||||
s_matrix[locality_radius,locality_radius,:,:,:] = \
|
||||
tools.cmath.expi(randomize_ang * (t.rand(obj_size)-0.5))
|
||||
|
||||
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((translations.shape[0],2)) - 0.5)
|
||||
|
||||
weights = t.ones(translations.shape[0])
|
||||
|
||||
if hasattr(datasets[0], 'mask') and datasets[0].mask is not None:
|
||||
mask = datasets[0].mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
|
||||
probe_support = t.zeros_like(probes[0,0].to(dtype=t.float32))
|
||||
|
||||
xs, ys = np.mgrid[:probes.shape[-3],:probes.shape[-2]]
|
||||
xs = xs - np.mean(xs)
|
||||
ys = ys - np.mean(ys)
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
probe_support[Rs<probe_convergence_radius] = 1
|
||||
probes = probes * probe_support[None,None,:,:]
|
||||
|
||||
|
||||
model = cls(wavelength, det_geo, probe_basis, probes, probe_support,
|
||||
s_matrix,
|
||||
detector_slice=det_slice,
|
||||
surface_normal=surface_normal,
|
||||
min_translation=min_translation,
|
||||
translation_offsets = translation_offsets,
|
||||
probe_planes = probe_planes,
|
||||
weights=weights, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
oversampling=oversampling)
|
||||
|
||||
# Now we need to produce a concatenated dataset to be used to
|
||||
# train the model
|
||||
dataset = Ptycho2DDataset(translations, patterns)
|
||||
return model, dataset
|
||||
|
||||
|
||||
def interaction(self, index, translations):
|
||||
pix_trans = tools.interactions.translations_to_pixel(self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans -= self.min_translation
|
||||
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += self.translation_scale * self.translation_offsets[index]
|
||||
if self.probe_planes is not None:
|
||||
probes_set = self.probe[self.probe_planes[index]]
|
||||
else:
|
||||
probes_set = self.probe[[0]*translations.shape[0]]
|
||||
|
||||
|
||||
all_exit_waves = []
|
||||
for i in range(probes_set.shape[1]):
|
||||
exit_waves = []
|
||||
|
||||
for j in range(probes_set.shape[0]):
|
||||
pr = tools.propagators.inverse_far_field(probes_set[j,i] * self.probe_fourier_support)
|
||||
exit_wave = self.probe_norm * tools.interactions.ptycho_2D_sinc_s_matrix(pr, self.s_matrix, pix_trans[j], shift_probe=True)
|
||||
exit_waves.append(exit_wave)
|
||||
|
||||
exit_waves = t.stack(exit_waves)
|
||||
|
||||
if exit_waves.dim() == 4:
|
||||
exit_waves = self.weights[index][:,None,None,None] * exit_waves
|
||||
else:
|
||||
exit_waves = self.weights[index] * exit_waves
|
||||
|
||||
all_exit_waves.append(exit_waves)
|
||||
|
||||
|
||||
return t.stack(all_exit_waves)
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(SMatrixPtycho, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
if self.probe_planes is not None:
|
||||
self.probe_planes = self.probe_planes.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_fourier_support = self.probe_fourier_support.to(*args,**kwargs)
|
||||
self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def corrected_translations(self,dataset):
|
||||
translations = dataset.translations.to(dtype=self.probe.dtype,device=self.probe.device)
|
||||
t_offset = tools.interactions.pixel_to_translations(self.probe_basis,self.translation_offsets*self.translation_scale,surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
|
||||
|
||||
# Needs to be updated to allow for plotting to an existing figure
|
||||
plot_list = [
|
||||
('First Dominant Probe Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe[0,0], fig=fig, basis=self.probe_basis)),
|
||||
('First Dominant Probe Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe[0,0], fig=fig, basis=self.probe_basis)),
|
||||
('Second Dominant Probe Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe[1,0], fig=fig, basis=self.probe_basis),
|
||||
lambda self: self.probe.shape[0] >=2),
|
||||
('Second Dominant Probe Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe[1,0], fig=fig, basis=self.probe_basis),
|
||||
lambda self: self.probe.shape[0] >=2),
|
||||
('Exit Wave Amplitude under Uniform Illumination',
|
||||
lambda self, fig: p.plot_amplitude(t.sum(self.s_matrix.data,dim=(0,1)), fig=fig, basis=self.probe_basis)),
|
||||
('Exit Wave Phase under Uniform Illumination',
|
||||
lambda self, fig: p.plot_phase(t.sum(self.s_matrix.data,dim=(0,1)), fig=fig, basis=self.probe_basis)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
probe = cmath.torch_to_complex(self.probe.detach().cpu())
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
s_matrix = cmath.torch_to_complex(self.s_matrix.detach().cpu())
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
wavelength = self.wavelength.cpu().numpy()
|
||||
|
||||
return {'basis':basis, 'translation':translations,
|
||||
'probe':probe,'s_matrix':s_matrix,
|
||||
'background':background,
|
||||
'weights':weights, 'wavelength':wavelength}
|
||||
@@ -0,0 +1,274 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from copy import copy
|
||||
from torch.utils import data as torchdata
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
|
||||
__all__ = ['SimplePtycho']
|
||||
|
||||
class SimplePtycho(CDIModel):
|
||||
"""A simple ptychography model for exploring ideas and extensions
|
||||
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis, detector_slice,
|
||||
probe_guess, obj_guess, min_translation = [0,0],
|
||||
surface_normal=np.array([0.,0.,1.]), mask=None):
|
||||
|
||||
super(SimplePtycho,self).__init__()
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.min_translation = t.tensor(min_translation)
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.detector_slice = copy(detector_slice)
|
||||
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
if mask is None:
|
||||
self.mask = None
|
||||
else:
|
||||
self.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)
|
||||
|
||||
# We rescale the probe here so it learns at the same rate as the
|
||||
# object
|
||||
self.probe_norm = t.max(t.abs(probe_guess))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset):
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
(indices, translations), patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
|
||||
center = tools.image_processing.centroid(t.sum(patterns,dim=0))
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center)
|
||||
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0.,0.,1.])
|
||||
|
||||
# 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)
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice)
|
||||
|
||||
obj = t.ones(obj_size).to(dtype=t.complex64)
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
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 -= self.min_translation
|
||||
|
||||
return tools.interactions.ptycho_2D_round(self.probe_norm * self.probe,
|
||||
self.obj,
|
||||
pix_trans)
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
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 to(self, *args, **kwargs):
|
||||
super(SimplePtycho, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.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.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
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)),
|
||||
('Probe Phase',
|
||||
lambda self, fig: p.plot_phase(self.probe, fig=fig, basis=self.probe_basis)),
|
||||
('Object Amplitude',
|
||||
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, basis=self.probe_basis))
|
||||
]
|
||||
|
||||
|
||||
|
||||
def save_results(self):
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
probe = probe * self.probe_norm.detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
return {'probe':probe,'obj':obj}
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,504 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools.datasets import Ptycho2DDataset
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools import analysis
|
||||
from matplotlib import pyplot as plt
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
from scipy import linalg as sla
|
||||
from copy import copy
|
||||
|
||||
#
|
||||
# Basic points:
|
||||
# Just one probe mode, no need to overcomplicate things.
|
||||
# Weights is only a list of numbers, no matrices or anything like that
|
||||
# Mandatory probe support in Fourier space
|
||||
#
|
||||
# When loading from a dataset, we need information on the zone plate geometry
|
||||
#
|
||||
|
||||
__all__ = ['TimeResolvedPtychoCalibration']
|
||||
|
||||
|
||||
class TimeResolvedPtychoCalibration(CDIModel):
|
||||
|
||||
def __init__(self, wavelength, detector_geometry,
|
||||
probe_basis,
|
||||
probe_guess, obj_guess,
|
||||
fourier_times, probe_fourier_support,
|
||||
times, time_dependence, frame_delays,
|
||||
detector_slice=None,
|
||||
surface_normal=t.tensor([0., 0., 1.], dtype=t.float32),
|
||||
min_translation=t.tensor([0, 0], dtype=t.float32),
|
||||
background=None, translation_offsets=None, mask=None,
|
||||
weights=None, translation_scale=1, saturation=None,
|
||||
oversampling=1,
|
||||
loss='amplitude mse', units='um',
|
||||
simulate_probe_translation=False):
|
||||
|
||||
super(TimeResolvedPtychoCalibration, 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:
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'], dtype=t.float32)
|
||||
|
||||
self.min_translation = t.tensor(min_translation)
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
self.detector_slice = copy(detector_slice)
|
||||
self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
self.units = units
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.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)
|
||||
|
||||
self.probe_norm = 1 * t.max(t.abs(probe_guess))
|
||||
|
||||
self.probe = t.nn.Parameter(probe_guess / self.probe_norm)
|
||||
self.obj = t.nn.Parameter(obj_guess)
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[self.detector_slice].shape,
|
||||
dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.nn.Parameter(background)
|
||||
|
||||
if weights is None:
|
||||
self.weights = None
|
||||
else:
|
||||
self.weights = t.nn.Parameter(t.tensor(weights,
|
||||
dtype=t.float32))
|
||||
|
||||
if translation_offsets is None:
|
||||
self.translation_offsets = None
|
||||
else:
|
||||
t_o = t.tensor(translation_offsets, dtype=t.float32)
|
||||
t_o = t_o / translation_scale
|
||||
self.translation_offsets = t.nn.Parameter(t_o)
|
||||
|
||||
self.translation_scale = translation_scale
|
||||
|
||||
self.probe_fourier_support = probe_fourier_support
|
||||
self.fourier_times = fourier_times
|
||||
self.times = times
|
||||
self.time_dependence = t.nn.Parameter(time_dependence)
|
||||
self.frame_delays = frame_delays
|
||||
|
||||
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, Js = t.meshgrid(Is/t.max(Is), Js/t.max(Js))
|
||||
self.I_phase = 2 * np.pi* Is
|
||||
self.J_phase = 2 * np.pi* Js
|
||||
|
||||
# Here we set the appropriate loss function
|
||||
if (loss.lower().strip() == 'amplitude mse'
|
||||
or loss.lower().strip() == 'amplitude_mse'):
|
||||
self.loss = tools.losses.amplitude_mse
|
||||
elif (loss.lower().strip() == 'poisson nll'
|
||||
or loss.lower().strip() == 'poisson_nll'):
|
||||
self.loss = tools.losses.poisson_nll
|
||||
else:
|
||||
raise KeyError('Specified loss function not supported')
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, zp_geometry, time_window, n_times, n_frames, randomize_ang=0, padding=0, translation_scale=1, saturation=None, propagation_distance=None, scattering_mode=None, oversampling=1, auto_center=False, opt_for_fft=False, loss='amplitude mse', units='um', simulate_probe_translation=False):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
|
||||
# We include the *extras to make this work even with datasets, like
|
||||
# polarization dependent datasets, that might toss out extra inputs
|
||||
(indices, translations, *extras), patterns = dataset[:]
|
||||
|
||||
dataset.get_as(*get_as_args[0], **get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns, dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
if hasattr(dataset, 'sample_info') and \
|
||||
dataset.sample_info is not None and \
|
||||
'orientation' in dataset.sample_info:
|
||||
surface_normal = dataset.sample_info['orientation'][2]
|
||||
else:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
|
||||
# If this information is supplied when the function is called,
|
||||
# then we override the information in the .cxi file
|
||||
if scattering_mode in {'t', 'transmission'}:
|
||||
surface_normal = np.array([0., 0., 1.])
|
||||
elif scattering_mode in {'r', 'reflection'}:
|
||||
outgoing_dir = np.cross(det_basis[:, 0], det_basis[:, 1])
|
||||
outgoing_dir /= np.linalg.norm(outgoing_dir)
|
||||
surface_normal = outgoing_dir + np.array([0., 0., 1.])
|
||||
surface_normal /= -np.linalg.norm(surface_normal)
|
||||
|
||||
# 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, padding=200)
|
||||
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
else:
|
||||
background = None
|
||||
|
||||
# Finally, initialize the probe and object using this information
|
||||
probe = tools.initializers.SHARP_style_probe(dataset, probe_shape, det_slice, propagation_distance=propagation_distance, oversampling=oversampling)
|
||||
|
||||
probe = tools.propagators.far_field(probe)
|
||||
|
||||
obj = t.exp(1j * randomize_ang * (t.rand(obj_size)-0.5))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
translation_offsets = 0 * (t.rand((len(dataset), 2)) - 0.5)
|
||||
# we define a set of weights which only has one index
|
||||
Ws = t.ones(len(dataset))
|
||||
|
||||
if hasattr(dataset, 'mask') and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
else:
|
||||
mask = None
|
||||
|
||||
# What do we know about the zp?
|
||||
# delta_r, N, and beamstop ratio. It's probably best, though,
|
||||
# to just read diameter, beamstop_diameter and focal_length directly
|
||||
# because that is the most general even if the optic isn't truly
|
||||
# a zone plate.
|
||||
zp_distance = zp_geometry['focal_length']
|
||||
probe_size = probe_basis * t.as_tensor(probe_shape, dtype=t.float32)
|
||||
pinv_basis = t.tensor(np.linalg.pinv(probe_size).transpose()).to(t.float32)
|
||||
zone_plate_basis = pinv_basis * wavelength * zp_distance
|
||||
zone_plate_steps = t.sum(zone_plate_basis,axis=1)
|
||||
# This may very well mix up x & y and fail on non-square detectors
|
||||
probe_fourier_support = t.zeros(probe.shape, dtype=t.bool)
|
||||
xs, ys = np.mgrid[:probe.shape[-2], :probe.shape[-1]]
|
||||
xs = zone_plate_steps[1] * (xs - np.mean(xs))
|
||||
ys = zone_plate_steps[0] * (ys - np.mean(ys))
|
||||
Rs = np.sqrt(xs**2 + ys**2)
|
||||
|
||||
distances = np.sqrt(zp_distance**2 + xs**2 + ys**2)
|
||||
times = (distances - t.min(distances)) / 2.99792e8
|
||||
|
||||
# This sets the support of the probe and also restricts the
|
||||
# timing matrix so it only considers times that are actually
|
||||
# in the window defined by the probe fourier support
|
||||
probe_fourier_support[Rs < zp_geometry['diameter']/2] = 1
|
||||
times[Rs > zp_geometry['diameter']/2] = 0
|
||||
times[Rs > zp_geometry['diameter']/2] = t.max(times)
|
||||
|
||||
probe_fourier_support[Rs < zp_geometry['beamstop_diameter']/2] = 0
|
||||
times[Rs < zp_geometry['beamstop_diameter']/2] = t.max(times)
|
||||
times[Rs < zp_geometry['beamstop_diameter']/2] = t.min(times)
|
||||
|
||||
fourier_times = times - t.min(times)
|
||||
probe = probe * probe_fourier_support
|
||||
|
||||
# This is now the time axis for the probe's envelope
|
||||
times = t.linspace(0, time_window, n_times+1)
|
||||
time_dependence = t.ones(n_times, dtype=t.complex64)
|
||||
frame_delays = t.linspace(0, t.max(fourier_times) + time_window, n_frames+2)
|
||||
frame_delays -= time_window
|
||||
frame_delays = frame_delays[1:-1]
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis, probe, obj,
|
||||
fourier_times, probe_fourier_support,
|
||||
times, time_dependence, frame_delays,
|
||||
detector_slice=det_slice,
|
||||
surface_normal=surface_normal,
|
||||
min_translation=min_translation,
|
||||
translation_offsets=translation_offsets,
|
||||
weights=Ws, mask=mask, background=background,
|
||||
translation_scale=translation_scale,
|
||||
saturation=saturation,
|
||||
oversampling=oversampling,
|
||||
loss=loss, units=units,
|
||||
simulate_probe_translation=simulate_probe_translation)
|
||||
|
||||
|
||||
def interaction(self, index, translations, *args):
|
||||
# The *args is included so that this can work even when given, say,
|
||||
# a polarized ptycho dataset that might spit out more inputs.
|
||||
|
||||
# Step 1 is to convert the translations for each position into a
|
||||
# value in pixels
|
||||
pix_trans = tools.interactions.translations_to_pixel(
|
||||
self.probe_basis,
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
pix_trans -= self.min_translation
|
||||
# We then add on any recovered translation offset, if they exist
|
||||
if self.translation_offsets is not None:
|
||||
pix_trans += (self.translation_scale *
|
||||
self.translation_offsets[index])
|
||||
|
||||
probes = self.get_probes(space='real')
|
||||
Ws = self.weights[index]
|
||||
|
||||
# This might not work well
|
||||
prs = Ws[...,None,None,None] * probes
|
||||
#prs = t.sum(Ws[..., None, None, None] * probes, axis=-3)
|
||||
#print(prs.shape)
|
||||
|
||||
if self.simulate_probe_translation:
|
||||
det_pix_trans = tools.interactions.translations_to_pixel(
|
||||
self.detector_geometry['basis'],
|
||||
translations,
|
||||
surface_normal=self.surface_normal)
|
||||
|
||||
probe_masks = t.exp(1j* (det_pix_trans[:,0,None,None] *
|
||||
self.I_phase[None,...] +
|
||||
det_pix_trans[:,1,None,None] *
|
||||
self.J_phase[None,...]))
|
||||
prs = prs * probe_masks[...,None,:,:]
|
||||
|
||||
|
||||
# Now we actually do the interaction, using the sinc subpixel
|
||||
# translation model as per usual
|
||||
exit_waves = self.probe_norm * tools.interactions.ptycho_2D_sinc(
|
||||
prs, self.obj, pix_trans,
|
||||
shift_probe=True, multiple_modes=True)
|
||||
return exit_waves
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
return tools.measurements.quadratic_background(
|
||||
wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
|
||||
# Note: No "loss" function is defined here, because it is added
|
||||
# dynamically during object creation in __init__
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(TimeResolvedPtychoCalibration, 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:
|
||||
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_fourier_support = self.probe_fourier_support.to(*args,
|
||||
**kwargs)
|
||||
self.fourier_times = self.fourier_times.to(*args, **kwargs)
|
||||
self.times = self.times.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.)
|
||||
|
||||
# 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
|
||||
|
||||
data = []
|
||||
len(indices)
|
||||
if calculation_width is None:
|
||||
calculation_width = len(indices)
|
||||
index_chunks = [indices[i:i + calculation_width]
|
||||
for i in range(0, len(indices),
|
||||
calculation_width)]
|
||||
translation_chunks = [translations[i:i + calculation_width]
|
||||
for i in range(0, len(indices),
|
||||
calculation_width)]
|
||||
|
||||
|
||||
# Then we simulate the results
|
||||
data = [self.forward(idx, trans).detach()
|
||||
for idx, trans in zip(index_chunks, translation_chunks)]
|
||||
|
||||
data = t.cat(data, dim=0)
|
||||
# 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)
|
||||
|
||||
|
||||
def corrected_translations(self, dataset):
|
||||
translations = dataset.translations.to(
|
||||
dtype=t.float32, device=self.probe.device)
|
||||
if (hasattr(self, 'translation_offsets') and
|
||||
self.translation_offsets is not None):
|
||||
t_offset = tools.interactions.pixel_to_translations(
|
||||
self.probe_basis,
|
||||
self.translation_offsets * self.translation_scale,
|
||||
surface_normal=self.surface_normal)
|
||||
return translations + t_offset
|
||||
else:
|
||||
return translations
|
||||
|
||||
|
||||
def get_probes(self, space='real'):
|
||||
# This is the part where I need to create the probe modes using the
|
||||
# time-dependent stuff
|
||||
|
||||
# This restricts the basis probes to stay within the probe support
|
||||
optic_mask = self.probe * self.probe_fourier_support
|
||||
|
||||
probes = t.zeros([len(self.frame_delays)] + list(self.probe.shape),
|
||||
dtype=self.probe.dtype, device=self.probe.device)
|
||||
for i, delay in enumerate(self.frame_delays):
|
||||
indices = t.bucketize(self.fourier_times, self.times + delay)
|
||||
clamped_indices = t.clamp(indices-1, max=len(self.time_dependence)-1)
|
||||
illumination = t.take(self.time_dependence, clamped_indices)
|
||||
illumination[indices==0] = 0
|
||||
illumination[indices==len(self.time_dependence)+1] = 0
|
||||
probes[i] = illumination * optic_mask
|
||||
|
||||
if space.lower()=='real':
|
||||
return tools.propagators.inverse_far_field(probes)
|
||||
elif space.lower()=='fourier' or space.lower()=='reciprocal':
|
||||
return probes
|
||||
|
||||
|
||||
plot_list = [
|
||||
('Probe Amplitudes (scroll to view modes)',
|
||||
lambda self, fig: p.plot_amplitude(self.get_probes(space='real'), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Probe Phases (scroll to view modes)',
|
||||
lambda self, fig: p.plot_phase(self.get_probes(space='real'), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Fourier Probe Amplitudes (scroll to view modes)',
|
||||
lambda self, fig: p.plot_amplitude(self.get_probes(space='fourier'), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Fourier Probe Phases (scroll to view modes)',
|
||||
lambda self, fig: p.plot_phase(self.get_probes(space='fourier'), fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Optic Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.probe, fig=fig, basis=self.probe_basis, units=self.units)),
|
||||
('Optic Phase',
|
||||
lambda self, fig: p.plot_phase(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: 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: len(self.weights.shape) >= 2),
|
||||
('Object Amplitude',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, 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)),
|
||||
('Corrected Translations',
|
||||
lambda self, fig, dataset: p.plot_translations(self.corrected_translations(dataset), fig=fig, units=self.units)),
|
||||
('Background',
|
||||
lambda self, fig: plt.figure(fig.number) and plt.imshow(self.background.detach().cpu().numpy()**2)),
|
||||
('Time Structure',
|
||||
lambda self, fig: (plt.figure(fig.number) and (plt.clf() or True) and plt.plot(self.time_dependence.real.detach().cpu().numpy()) and plt.plot(self.time_dependence.imag.detach().cpu().numpy())))
|
||||
]
|
||||
|
||||
# def plot_errors(self, dataset):
|
||||
|
||||
|
||||
|
||||
def save_results(self, dataset):
|
||||
basis = self.probe_basis.detach().cpu().numpy()
|
||||
translations = self.corrected_translations(dataset).detach().cpu().numpy()
|
||||
optic = self.probe.detach().cpu().numpy()
|
||||
optic = optic * self.probe_norm.detach().cpu().numpy()
|
||||
time_dependence = self.time_dependence.detach().cpu().numpy()
|
||||
times = self.times.detach().cpu().numpy()
|
||||
fourier_times = self.fourier_times.detach().cpu().numpy()
|
||||
probes = self.get_probes().detach().cpu().numpy()
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
weights = self.weights.detach().cpu().numpy()
|
||||
|
||||
return {'basis': basis, 'translation': translations,
|
||||
'probes': probes, 'optic': optic,
|
||||
'times': times, 'time_dependence': time_dependence,
|
||||
'fourier_times': fourier_times,
|
||||
'obj': obj,
|
||||
'background': background,
|
||||
'weights': weights,
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
import torch as t
|
||||
from CDTools.models import CDIModel
|
||||
from CDTools import tools
|
||||
from CDTools.tools import plotting as p
|
||||
from CDTools.tools.interactions import RPI_interaction
|
||||
from CDTools.tools import initializers
|
||||
from scipy.ndimage.morphology import binary_dilation
|
||||
import numpy as np
|
||||
from copy import copy
|
||||
|
||||
__all__ = ['MultimodeRPI']
|
||||
|
||||
|
||||
|
||||
__all__ = ['RPI']
|
||||
|
||||
class TimeResolvedRPI(CDIModel):
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
return t.complex(self.obj_real, self.obj_imag)
|
||||
|
||||
|
||||
def __init__(self, wavelength, detector_geometry, probe_basis,
|
||||
probe, obj_guess, framerate, detector_slice=None,
|
||||
background=None, mask=None, saturation=None,
|
||||
obj_support=None, oversampling=1):
|
||||
|
||||
super(TimeResolvedRPI, self).__init__()
|
||||
|
||||
self.wavelength = t.tensor(wavelength)
|
||||
self.framerate = framerate
|
||||
self.detector_geometry = copy(detector_geometry)
|
||||
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = t.tensor(det_geo['distance'])
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = t.tensor(det_geo['basis'])
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = t.tensor(det_geo['corner'])
|
||||
|
||||
self.probe_basis = t.tensor(probe_basis)
|
||||
|
||||
scale_factor = t.tensor([probe.shape[-1]/obj_guess.shape[-1],
|
||||
probe.shape[-2]/obj_guess.shape[-2]])
|
||||
self.obj_basis = self.probe_basis * scale_factor
|
||||
self.detector_slice = detector_slice
|
||||
|
||||
# Maybe something to include in a bit
|
||||
# self.surface_normal = t.tensor(surface_normal)
|
||||
|
||||
self.saturation = saturation
|
||||
|
||||
if mask is None:
|
||||
self.mask = mask
|
||||
else:
|
||||
self.mask = t.tensor(mask, dtype=t.bool)
|
||||
|
||||
|
||||
self.probe = t.tensor(probe, dtype=t.complex64)
|
||||
|
||||
obj_guess = t.tensor(obj_guess, dtype=t.complex64)
|
||||
|
||||
self.obj_real = t.nn.Parameter(obj_guess.real)
|
||||
self.obj_imag = t.nn.Parameter(obj_guess.imag)
|
||||
|
||||
# Wait for LBFGS to be updated for complex-valued parameters
|
||||
# self.obj = t.nn.Parameter(obj_guess.to(t.float32))
|
||||
|
||||
if background is None:
|
||||
if detector_slice is not None:
|
||||
background = 1e-6 * t.ones(
|
||||
self.probe[0][self.detector_slice].shape,
|
||||
dtype=t.float32)
|
||||
else:
|
||||
background = 1e-6 * t.ones(self.probe[0].shape,
|
||||
dtype=t.float32)
|
||||
|
||||
self.background = t.tensor(background, dtype=t.float32)
|
||||
|
||||
if obj_support is not None:
|
||||
self.obj_support = obj_support
|
||||
self.obj.data = self.obj * obj_support[None, ...]
|
||||
else:
|
||||
self.obj_support = t.ones_like(self.obj[0, ...])
|
||||
|
||||
self.oversampling = oversampling
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_dataset(cls, dataset, probe, framerate, obj_size=None, background=None, mask=None, padding=0, saturation=None, scattering_mode=None, oversampling=1, auto_center=False, initialization='random', opt_for_fft=False, probe_threshold=0):
|
||||
|
||||
wavelength = dataset.wavelength
|
||||
det_basis = dataset.detector_geometry['basis']
|
||||
det_shape = dataset[0][1].shape
|
||||
distance = dataset.detector_geometry['distance']
|
||||
|
||||
# always do this on the cpu
|
||||
get_as_args = dataset.get_as_args
|
||||
dataset.get_as(device='cpu')
|
||||
# We only need the patterns here, not the inputs associated with them.
|
||||
_, patterns = dataset[:]
|
||||
dataset.get_as(*get_as_args[0],**get_as_args[1])
|
||||
|
||||
# Set to none to avoid issues with things outside the detector
|
||||
if auto_center:
|
||||
center = tools.image_processing.centroid(t.sum(patterns,dim=0))
|
||||
else:
|
||||
center = None
|
||||
|
||||
# Then, generate the probe geometry from the dataset
|
||||
ewg = tools.initializers.exit_wave_geometry
|
||||
probe_basis, probe_shape, det_slice = ewg(det_basis,
|
||||
det_shape,
|
||||
wavelength,
|
||||
distance,
|
||||
center=center,
|
||||
padding=padding,
|
||||
opt_for_fft=opt_for_fft,
|
||||
oversampling=oversampling)
|
||||
|
||||
if not isinstance(probe,t.Tensor):
|
||||
probe = t.as_tensor(probe)
|
||||
|
||||
if background is None and hasattr(dataset, 'background') \
|
||||
and dataset.background is not None:
|
||||
background = t.sqrt(dataset.background)
|
||||
elif background is not None:
|
||||
background = t.sqrt(t.Tensor(background).to(dtype=t.float32))
|
||||
|
||||
det_geo = dataset.detector_geometry
|
||||
|
||||
# If no mask is given, but one exists in the dataset, load it.
|
||||
if mask is None and hasattr(dataset, 'mask') \
|
||||
and dataset.mask is not None:
|
||||
mask = dataset.mask.to(t.bool)
|
||||
|
||||
# Now we initialize the object
|
||||
if obj_size is None:
|
||||
# This is a standard size for a well-matched probe and detector
|
||||
obj_size = list((np.array(probe_shape) // 2).astype(int))
|
||||
|
||||
# I think something to do with the fact that the object is defined
|
||||
# on a coarser grid needs to be accounted for here that is not
|
||||
# accounted for yet
|
||||
scale = t.sum(patterns[0]) / t.sum(t.abs(probe)**2)
|
||||
n_modes = (probe.shape[0] - 1) // framerate + 1
|
||||
|
||||
obj_guess = scale * t.exp(2j * np.pi * t.rand([n_modes,]+obj_size))
|
||||
|
||||
|
||||
probe_intensity = t.sqrt(t.sum(t.abs(probe)**2,axis=0))
|
||||
probe_fft = tools.propagators.far_field(probe_intensity)
|
||||
pad0l = (probe.shape[-2] - obj_size[-2])//2
|
||||
pad0r = probe.shape[-2] - obj_size[-2] - pad0l
|
||||
pad1l = (probe.shape[-1] - obj_size[-1])//2
|
||||
pad1r = probe.shape[-1] - obj_size[-1] - pad1l
|
||||
probe_lr_fft = probe_fft[pad0l:-pad0r,pad1l:-pad1r]
|
||||
probe_lr = t.abs(tools.propagators.inverse_far_field(probe_lr_fft))
|
||||
|
||||
obj_support = probe_lr > t.max(probe_lr) * probe_threshold
|
||||
obj_support = t.as_tensor(binary_dilation(obj_support))
|
||||
|
||||
return cls(wavelength, det_geo, probe_basis,
|
||||
probe, obj_guess, framerate, detector_slice=det_slice,
|
||||
background=background, mask=mask, saturation=saturation,
|
||||
obj_support=obj_support, oversampling=oversampling)
|
||||
|
||||
|
||||
def random_init(self, pattern):
|
||||
scale = t.sum(pattern) / t.sum(t.abs(self.probe)**2)
|
||||
self.obj.data = scale * t.exp(
|
||||
2j * np.pi * t.rand(self.obj.shape)).to(
|
||||
dtype=self.obj.dtype, device=self.obj.device)
|
||||
|
||||
|
||||
# Needs work
|
||||
def interaction(self, index, *args):
|
||||
# including *args allows this to work with all sorts of datasets
|
||||
# that might include other information in with the index in their
|
||||
# "input" parameters (such as translations for a ptychography dataset).
|
||||
# This makes it seamless to use such a dataset even though those
|
||||
# extra arguments will not be used.
|
||||
|
||||
|
||||
all_exit_waves = []
|
||||
|
||||
# Mix the probes with the weight matrix
|
||||
prs = self.probe
|
||||
|
||||
for i in range(self.probe.shape[0]):
|
||||
obj_frame = i // self.framerate
|
||||
pr = prs[i]
|
||||
exit_waves = RPI_interaction(pr,
|
||||
self.obj_support * self.obj[obj_frame])
|
||||
all_exit_waves.append(exit_waves.unsqueeze(0))
|
||||
|
||||
# This creates a bunch of modes generated from all possible combos
|
||||
# of the probe and object modes all strung out along the first index
|
||||
|
||||
output = t.cat(all_exit_waves)
|
||||
|
||||
# If we have multiple indexes input, we unsqueeze and repeat the stack
|
||||
# of wavefields enough times to simulate each requested index. This
|
||||
# seems silly, but it enables (for example) one to do a reconstruction
|
||||
# from a set of diffraction patterns that are all known to be from the
|
||||
# same object.
|
||||
try:
|
||||
# will fail if index has no length, for example when index
|
||||
# is just an int. In this case, we just do nothing instead
|
||||
output = output.unsqueeze(0).repeat(1,len(index),1,1,1)
|
||||
except TypeError:
|
||||
pass
|
||||
return output
|
||||
|
||||
|
||||
def forward_propagator(self, wavefields):
|
||||
return tools.propagators.far_field(wavefields)
|
||||
|
||||
|
||||
def backward_propagator(self, wavefields):
|
||||
return tools.propagators.inverse_far_field(wavefields)
|
||||
|
||||
|
||||
def measurement(self, wavefields):
|
||||
# Here I'm taking advantage of an undocumented feature in the
|
||||
# incoherent_sum measurement function where it will work with
|
||||
# a 4D wavefield array as well as a 5D array.
|
||||
return tools.measurements.quadratic_background(wavefields,
|
||||
self.background,
|
||||
detector_slice=self.detector_slice,
|
||||
measurement=tools.measurements.incoherent_sum,
|
||||
saturation=self.saturation,
|
||||
oversampling=self.oversampling)
|
||||
|
||||
def loss(self, sim_data, real_data, mask=None):
|
||||
return tools.losses.amplitude_mse(real_data, sim_data, mask=mask)
|
||||
#return tools.losses.poisson_nll(real_data, sim_data, mask=mask)
|
||||
|
||||
def regularizer(self, factors):
|
||||
return factors[0] * t.sum(t.abs(self.obj[0,:,:])**2) \
|
||||
+ factors[1] * t.sum(t.abs(self.obj[1:,:,:])**2)
|
||||
|
||||
def to(self, *args, **kwargs):
|
||||
super(TimeResolvedRPI, self).to(*args, **kwargs)
|
||||
self.wavelength = self.wavelength.to(*args,**kwargs)
|
||||
# move the detector geometry too
|
||||
det_geo = self.detector_geometry
|
||||
if hasattr(det_geo, 'distance'):
|
||||
det_geo['distance'] = det_geo['distance'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'basis'):
|
||||
det_geo['basis'] = det_geo['basis'].to(*args,**kwargs)
|
||||
if hasattr(det_geo, 'corner'):
|
||||
det_geo['corner'] = det_geo['corner'].to(*args,**kwargs)
|
||||
|
||||
if self.mask is not None:
|
||||
self.mask = self.mask.to(*args, **kwargs)
|
||||
|
||||
self.probe = self.probe.to(*args,**kwargs)
|
||||
self.probe_basis = self.probe_basis.to(*args,**kwargs)
|
||||
self.obj_basis = self.obj_basis.to(*args,**kwargs)
|
||||
self.obj_support = self.obj_support.to(*args,**kwargs)
|
||||
self.background = self.background.to(*args, **kwargs)
|
||||
|
||||
# Maybe include in a bit
|
||||
#self.surface_normal = self.surface_normal.to(*args, **kwargs)
|
||||
|
||||
def sim_to_dataset(self, args_list):
|
||||
raise NotImplementedError('No sim to dataset yet, sorry!')
|
||||
|
||||
plot_list = [
|
||||
('Probe Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.probe, fig=fig, basis=self.probe_basis)),
|
||||
('Object Amplitudes',
|
||||
lambda self, fig: p.plot_amplitude(self.obj, fig=fig,
|
||||
basis=self.obj_basis)),
|
||||
('Object Phases',
|
||||
lambda self, fig: p.plot_phase(self.obj, fig=fig,
|
||||
basis=self.obj_basis))
|
||||
]
|
||||
|
||||
|
||||
def save_results(self, dataset=None, full_obj=False):
|
||||
# dataset is set as a kwarg here because it isn't needed, but the
|
||||
# common pattern is to pass a dataset. This makes it okay if one
|
||||
# continues to use that standard pattern
|
||||
probe_basis = self.probe_basis.detach().cpu().numpy()
|
||||
obj_basis = self.obj_basis.detach().cpu().numpy()
|
||||
probe = self.probe.detach().cpu().numpy()
|
||||
# Provide the option to save out the subdominant objects or
|
||||
# just the dominant one
|
||||
if full_obj:
|
||||
obj = self.obj.detach().cpu().numpy()
|
||||
else:
|
||||
obj = self.obj[0].detach().cpu().numpy()
|
||||
background = self.background.detach().cpu().numpy()**2
|
||||
|
||||
return {'probe_basis': probe_basis, 'obj_basis': obj_basis,
|
||||
'probe': probe,'obj': obj,
|
||||
'background': background}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
""" This module contains various packages of tools supporting common needs
|
||||
|
||||
It at times feels a bit silly to keep all the tools siloed under a particular
|
||||
subpackage such as "projectors" or "measurements", especially given that
|
||||
"flat is better than nested", but I find something comforting about the
|
||||
organization and the ability to only pull in the particular set of tools
|
||||
that one needs for a specific application.
|
||||
|
||||
The submodules are all structured as modules with their own __init__ files,
|
||||
which use an import * statement to import from a file defining the various
|
||||
functions. This is done to prevent leakage of imported packages into the
|
||||
namespace of CDTools. I know, we're all consenting adults, but I just hate
|
||||
having numpy and torch defined under CDTools.tools.cmath, you know?
|
||||
|
||||
"""
|
||||
|
||||
from CDTools.tools import losses
|
||||
from CDTools.tools import data
|
||||
from CDTools.tools import image_processing
|
||||
from CDTools.tools import initializers
|
||||
from CDTools.tools import plotting
|
||||
from CDTools.tools import interactions
|
||||
from CDTools.tools import propagators
|
||||
from CDTools.tools import measurements
|
||||
from CDTools.tools import analysis
|
||||
from CDTools.tools import atoms
|
||||
from CDTools.tools import polarization
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.analysis.analysis import *
|
||||
@@ -0,0 +1,960 @@
|
||||
"""Contains basic functions for analyzing the results of reconstructions
|
||||
|
||||
The functions in this module are designed to work either with pytorch tensors
|
||||
or numpy arrays, so they can be used either directly after reconstructions
|
||||
on the attributes of the models themselves, or after-the-fact once the
|
||||
data has been stored in numpy arrays.
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
import numpy as np
|
||||
from CDTools.tools import image_processing as ip
|
||||
from scipy import fftpack
|
||||
from scipy import linalg as sla
|
||||
from scipy import special
|
||||
|
||||
__all__ = ['orthogonalize_probes', 'standardize', 'synthesize_reconstructions',
|
||||
'calc_consistency_prtf', 'calc_deconvolved_cross_correlation',
|
||||
'calc_frc', 'calc_vn_entropy', 'calc_top_mode_fraction',
|
||||
'calc_rms_error', 'calc_fidelity', 'calc_generalized_rms_error']
|
||||
|
||||
|
||||
def orthogonalize_probes(probes, density_matrix=None, keep_transform=False, normalize=False):
|
||||
"""Orthogonalizes a set of incoherently mixing probes
|
||||
|
||||
The strategy is to define a reduced orthogonal basis that spans
|
||||
all of the retrieved probes, and then build the density matrix
|
||||
defined by the probes in that basis. After diagonalization, the
|
||||
eigenvectors can be recast into the original basis and returned
|
||||
|
||||
By default, it assumes that the set of probes are defined just as
|
||||
standard incoherently mixing probe modes, and orthogonalizes them.
|
||||
However, if a density matrix is explicitly given, it will instead
|
||||
consider the problem of extracting the eigenbasis of the matrix
|
||||
probes * denstity_matrix * probes^dagger, where probes is the
|
||||
column matrix of the given probe functions. This latter problem arises
|
||||
in the generalization of the probe mixing model, and reduces to the
|
||||
simpler case when the density matrix is equal to the identity matrix
|
||||
|
||||
If the parameter "keep_transform" is set, the function will additionally
|
||||
return the matrix A such that A * ortho_probes^dagger = probes^dagger
|
||||
|
||||
If the parameter "normalize" is False (as is the default), the variation
|
||||
in intensities in the probe modes will be kept in the probe modes, as is
|
||||
natural for a purely incoherent model. If it is set to "True", the
|
||||
returned probe modes will all be normalized instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probes : array
|
||||
An l x n x m complex array representing a stack of probes
|
||||
density_matrix : np.array
|
||||
An optional l x l density matrix further elaborating on the state
|
||||
keep_transform : bool
|
||||
Default False, whether to return the map from probes to ortho_probes
|
||||
normalize : bool
|
||||
Default False, whether to normalize the probe modes
|
||||
|
||||
Returns
|
||||
-------
|
||||
ortho_probes: array
|
||||
An l x n x m complex array representing a stack of probes
|
||||
"""
|
||||
|
||||
try:
|
||||
probes = probes.detach().cpu().numpy()
|
||||
send_to_torch = True
|
||||
except:
|
||||
send_to_torch = False
|
||||
|
||||
# We can do the orthogonalization with an SVD, so first we have to
|
||||
# reshape the final two dimensions (the image shape) into a single
|
||||
# vectorized dimension. This matrix is probes^dagger, hence the
|
||||
# conjugation
|
||||
probes_mat = probes.reshape(probes.shape[0],
|
||||
probes.shape[1]*probes.shape[2])
|
||||
|
||||
if density_matrix is None:
|
||||
density_matrix = np.eye(probes.shape[0])
|
||||
|
||||
|
||||
# next we want to extract the eigendecomposition of the density matrix
|
||||
# itself
|
||||
w,v = sla.eigh(density_matrix)
|
||||
w = w[::-1]
|
||||
v = v[:,::-1]
|
||||
|
||||
# We do this just to avoid total failure when the density
|
||||
# matrix is not positive definite.
|
||||
# In most cases (such as when rho is generated directly from some other
|
||||
# matrix A such that rho=A A^dagger), w should never have any negative
|
||||
# entries.
|
||||
w = np.maximum(w,0)
|
||||
|
||||
B_dagger = np.dot(np.diag(np.sqrt(w)), v.conj().transpose())
|
||||
|
||||
#u,s,vh = np.linalg.svd(np.dot(B_dagger,probes_mat), full_matrices=False)
|
||||
u,s,vh = sla.svd(np.dot(B_dagger,probes_mat), full_matrices=False)
|
||||
|
||||
|
||||
if normalize:
|
||||
ortho_probes = vh.reshape(probes.shape[0],
|
||||
probes.shape[1],
|
||||
probes.shape[2])
|
||||
|
||||
B_dagger_inv = np.linalg.pinv(B_dagger)
|
||||
A = np.dot(B_dagger_inv,np.dot(u,np.diag(s)))
|
||||
#A_dagger = np.dot(np.linalg.pinv(np.diag(s)),
|
||||
# np.dot(np.transpose(u).conj(),B_dagger))
|
||||
else:
|
||||
ortho_probes = np.dot(np.diag(s),vh).reshape(probes.shape[0],
|
||||
probes.shape[1],
|
||||
probes.shape[2])
|
||||
B_dagger_inv = np.linalg.pinv(B_dagger)
|
||||
A = np.dot(B_dagger_inv,u)
|
||||
#A_dagger = np.dot(np.transpose(u).conj(),B_dagger)
|
||||
|
||||
if send_to_torch:
|
||||
ortho_probes = t.as_tensor(np.stack(ortho_probes))
|
||||
A = t.as_tensor(A)
|
||||
|
||||
if keep_transform:
|
||||
return ortho_probes, A#_dagger
|
||||
else:
|
||||
return ortho_probes
|
||||
|
||||
|
||||
def standardize(probe, obj, obj_slice=None, correct_ramp=False):
|
||||
"""Standardizes a probe and object to prepare them for comparison
|
||||
|
||||
There are a number of ambiguities in the definition of a ptychographic
|
||||
reconstruction. This function makes an explicit choice for each ambiguity
|
||||
to allow comparisons between independent reconstructions without confusing
|
||||
these ambiguities for real differences between the reconstructions.
|
||||
|
||||
The ambiguities and standardizations are:
|
||||
|
||||
1) a. Probe and object can be scaled inversely to one another
|
||||
b. So we set the probe intensity to an average per-pixel value of 1
|
||||
2) a. The probe and object can aquire equal and opposite phase ramps
|
||||
b. So we set the centroid of the FFT of the probe to zero frequency
|
||||
3) a. The probe and object can each acquire an arbitrary overall phase
|
||||
b. So we set the phase of the sum of all values of both the probe and object to 0
|
||||
|
||||
When dealing with the properties of the object, a slice is used by
|
||||
default as the edges of the object often are dominated by unphysical
|
||||
noise. The default slice is from 3/8 to 5/8 of the way across. If the
|
||||
probe is actually a stack of incoherently mixing probes, then the
|
||||
dominant probe mode (assumed to be the first in the list) is used, but
|
||||
all the probes are updated with the same factors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : array
|
||||
A complex array storing a retrieved probe or stack of incoherently mixed probes
|
||||
obj : array
|
||||
A complex array storing a retrieved object
|
||||
obj_slice : slice
|
||||
Optional, a slice to take from the object for calculating normalizations
|
||||
correct_ramp : bool
|
||||
Default False, whether to correct for the relative phase ramps
|
||||
|
||||
Returns
|
||||
-------
|
||||
standardized_probe : array
|
||||
The standardized probe
|
||||
standardized_obj : array
|
||||
The standardized object
|
||||
|
||||
"""
|
||||
# First, we normalize the probe intensity to a fixed value.
|
||||
probe_np = False
|
||||
if isinstance(probe, np.ndarray):
|
||||
probe = t.as_tensor(probe, dtype=t.complex64)
|
||||
probe_np = True
|
||||
obj_np = False
|
||||
if isinstance(obj, np.ndarray):
|
||||
obj = t.as_tensor(obj,dtype=t.complex64)
|
||||
obj_np = True
|
||||
|
||||
# If this is a single probe and not a stack of probes
|
||||
if len(probe.shape) == 2:
|
||||
probe = probe[None,...]
|
||||
single_probe = True
|
||||
else:
|
||||
single_probe = False
|
||||
|
||||
normalization = t.sqrt(t.mean(t.abs(probe[0])**2))
|
||||
probe = probe / normalization
|
||||
obj = obj * normalization
|
||||
|
||||
# Default slice of the object to use for alignment, etc.
|
||||
if obj_slice is None:
|
||||
obj_slice = np.s_[(obj.shape[0]//8)*3:(obj.shape[0]//8)*5,
|
||||
(obj.shape[1]//8)*3:(obj.shape[1]//8)*5]
|
||||
|
||||
|
||||
if correct_ramp:
|
||||
# Need to check if this is actually working and, if not, why not
|
||||
center_freq = ip.centroid(t.abs(t.fft.fftshift(t.fft.fft2(probe[0]),
|
||||
dim=(-1,-2)))**2)
|
||||
center_freq -= t.div(t.tensor(probe[0].shape,dtype=t.float32),2,rounding_mode='floor')
|
||||
center_freq /= t.as_tensor(probe[0].shape,dtype=t.float32)
|
||||
|
||||
Is, Js = np.mgrid[:probe[0].shape[0],:probe[0].shape[1]]
|
||||
probe_phase_ramp = t.exp(2j * np.pi *
|
||||
(center_freq[0] * t.tensor(Is).to(t.float32) +
|
||||
center_freq[1] * t.tensor(Js).to(t.float32)))
|
||||
probe = probe * t.conj(probe_phase_ramp)
|
||||
Is, Js = np.mgrid[:obj.shape[0],:obj.shape[1]]
|
||||
obj_phase_ramp = t.exp(2j*np.pi *
|
||||
(center_freq[0] * t.tensor(Is).to(t.float32) +
|
||||
center_freq[1] * t.tensor(Js).to(t.float32)))
|
||||
obj = obj * obj_phase_ramp
|
||||
|
||||
# Then, we set them to consistent absolute phases
|
||||
|
||||
obj_angle = t.angle(t.sum(obj[obj_slice]))
|
||||
obj = obj * t.exp(-1j*obj_angle)
|
||||
|
||||
for i in range(probe.shape[0]):
|
||||
probe_angle = t.angle(t.sum(probe[i]))
|
||||
probe[i] = probe[i] * t.exp(-1j*probe_angle)
|
||||
|
||||
if single_probe:
|
||||
probe = probe[0]
|
||||
|
||||
if probe_np:
|
||||
probe = probe.detach().cpu().numpy()
|
||||
if obj_np:
|
||||
obj = obj.detach().cpu().numpy()
|
||||
|
||||
return probe, obj
|
||||
|
||||
|
||||
|
||||
def synthesize_reconstructions(probes, objects, use_probe=False, obj_slice=None, correct_ramp=False):
|
||||
"""Takes a collection of reconstructions and outputs a single synthesized probe and object
|
||||
|
||||
The function first standardizes the sets of probes and objects using the
|
||||
standardize function, passing through the relevant options. Then it
|
||||
calculates the closest overlap of subsequent frames to subpixel
|
||||
precision and uses a sinc interpolation to shift all the probes and objects
|
||||
to a common frame. Then the images are summed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probes : list(array)
|
||||
A list of probes or stacks of probe modes
|
||||
objects : list(array)
|
||||
A list of objects
|
||||
use_probe : bool
|
||||
Default False, whether to use the probe or object for alignment
|
||||
obj_slice : slice
|
||||
Optional, A slice of the object to use for alignment and normalization
|
||||
correct_ramp : bool
|
||||
Default False, whether to correct for a relative phase ramp in the probe and object
|
||||
|
||||
Returns
|
||||
-------
|
||||
synth_probe : array
|
||||
The synthesized probe
|
||||
synth_obj : array
|
||||
The synthesized object
|
||||
obj_stack : list(array)
|
||||
A list of standardized objects, for further processing
|
||||
"""
|
||||
|
||||
# This should be cleaned up so it accepts anything array_like
|
||||
probe_np = False
|
||||
if isinstance(probes[0], np.ndarray):
|
||||
probes = [t.as_tensor(probe,dtype=t.complex64) for probe in probes]
|
||||
probe_np = True
|
||||
obj_np = False
|
||||
if isinstance(objects[0], np.ndarray):
|
||||
objects = [t.as_tensor(obj,dtype=t.complex64) for obj in objects]
|
||||
obj_np = True
|
||||
|
||||
obj_shape = np.min(np.array([obj.shape for obj in objects]),axis=0)
|
||||
objects = [obj[:obj_shape[0],:obj_shape[1]] for obj in objects]
|
||||
|
||||
if obj_slice is None:
|
||||
obj_slice = np.s_[(objects[0].shape[0]//8)*3:(objects[0].shape[0]//8)*5,
|
||||
(objects[0].shape[1]//8)*3:(objects[0].shape[1]//8)*5]
|
||||
|
||||
|
||||
|
||||
synth_probe, synth_obj = standardize(probes[0].clone(), objects[0].clone(), obj_slice=obj_slice,correct_ramp=correct_ramp)
|
||||
|
||||
obj_stack = [synth_obj]
|
||||
|
||||
for i, (probe, obj) in enumerate(zip(probes[1:],objects[1:])):
|
||||
probe, obj = standardize(probe.clone(), obj.clone(), obj_slice=obj_slice,correct_ramp=correct_ramp)
|
||||
if use_probe:
|
||||
shift = ip.find_shift(synth_probe[0],probe[0], resolution=50)
|
||||
else:
|
||||
shift = ip.find_shift(synth_obj[obj_slice],obj[obj_slice], resolution=50)
|
||||
|
||||
|
||||
obj = ip.sinc_subpixel_shift(obj,np.array(shift))
|
||||
|
||||
if len(probe.shape) == 3:
|
||||
probe = t.stack([ip.sinc_subpixel_shift(p,tuple(shift))
|
||||
for p in probe],dim=0)
|
||||
else:
|
||||
probe = ip.sinc_subpixel_shift(probe,tuple(shift))
|
||||
|
||||
synth_probe = synth_probe + probe
|
||||
synth_obj = synth_obj + obj
|
||||
obj_stack.append(obj)
|
||||
|
||||
|
||||
# If there only was one image
|
||||
try:
|
||||
i
|
||||
except:
|
||||
i = -1
|
||||
|
||||
if probe_np:
|
||||
synth_probe = synth_probe.numpy()
|
||||
if obj_np:
|
||||
synth_obj = synth_obj.numpy()
|
||||
obj_stack = [obj.numpy() for obj in obj_stack]
|
||||
|
||||
return synth_probe/(i+2), synth_obj/(i+2), obj_stack
|
||||
|
||||
|
||||
|
||||
def calc_consistency_prtf(synth_obj, objects, basis, obj_slice=None,nbins=None):
|
||||
"""Calculates a PRTF between each the individual objects and a synthesized one
|
||||
|
||||
The consistency PRTF at any given spatial frequency is defined as the ratio
|
||||
between the intensity of any given reconstruction and the intensity
|
||||
of a synthesized or averaged reconstruction at that spatial frequency.
|
||||
Typically, the PRTF is averaged over spatial frequencies with the same
|
||||
magnitude.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
synth_obj : array
|
||||
The synthesized object in the numerator of the PRTF
|
||||
objects : list(array)
|
||||
A list of objects or diffraction patterns for the denomenator of the PRTF
|
||||
basis : array
|
||||
The basis for the reconstruction array to allow output in physical unit
|
||||
obj_slice : slice
|
||||
Optional, a slice of the objects to use for calculating the PRTF
|
||||
nbins : int
|
||||
Optional, number of bins to use in the histogram. Defaults to a sensible value
|
||||
|
||||
Returns
|
||||
-------
|
||||
freqs : array
|
||||
The frequencies for the PRTF
|
||||
PRTF : array
|
||||
The values of the PRTF
|
||||
"""
|
||||
|
||||
obj_np = False
|
||||
if isinstance(objects[0], np.ndarray):
|
||||
objects = [t.as_tensor(obj, dtype=t.complex64) for obj in objects]
|
||||
obj_np = True
|
||||
if isinstance(synth_obj, np.ndarray):
|
||||
synth_obj = t.as_tensor(synth_obj, dtype=t.complex64)
|
||||
|
||||
if isinstance(basis, t.Tensor):
|
||||
basis = basis.detach().cpu().numpy()
|
||||
|
||||
if obj_slice is None:
|
||||
obj_slice = np.s_[(objects[0].shape[0]//8)*3:(objects[0].shape[0]//8)*5,
|
||||
(objects[0].shape[1]//8)*3:(objects[0].shape[1]//8)*5]
|
||||
|
||||
if nbins is None:
|
||||
nbins = np.max(synth_obj[obj_slice].shape) // 4
|
||||
|
||||
synth_fft = (t.abs(t.fft.fftshift(t.fft.fft2(synth_obj[obj_slice]), dim=(-1,-2)))**2).numpy()
|
||||
|
||||
|
||||
di = np.linalg.norm(basis[:,0])
|
||||
dj = np.linalg.norm(basis[:,1])
|
||||
|
||||
i_freqs = fftpack.fftshift(fftpack.fftfreq(synth_fft.shape[0],d=di))
|
||||
j_freqs = fftpack.fftshift(fftpack.fftfreq(synth_fft.shape[1],d=dj))
|
||||
|
||||
Js,Is = np.meshgrid(j_freqs,i_freqs)
|
||||
Rs = np.sqrt(Is**2+Js**2)
|
||||
|
||||
|
||||
synth_ints, bins = np.histogram(Rs,bins=nbins,weights=synth_fft)
|
||||
|
||||
prtfs = []
|
||||
for obj in objects:
|
||||
obj = obj[obj_slice]
|
||||
single_fft = (t.abs(t.fft.fftshift(t.fft.fft2(obj),
|
||||
dim=(-1,-2)))**2).numpy()
|
||||
single_ints, bins = np.histogram(Rs,bins=nbins,weights=single_fft)
|
||||
|
||||
prtfs.append(synth_ints/single_ints)
|
||||
|
||||
|
||||
prtf = np.mean(prtfs,axis=0)
|
||||
|
||||
if not obj_np:
|
||||
bins = t.Tensor(bins)
|
||||
prtf = t.Tensor(prtf)
|
||||
|
||||
return bins[:-1], prtf
|
||||
|
||||
|
||||
|
||||
def calc_deconvolved_cross_correlation(im1, im2, im_slice=None):
|
||||
"""Calculates a cross-correlation between two images with their autocorrelations deconvolved.
|
||||
|
||||
This is formally defined as the inverse Fourier transform of the normalized
|
||||
product of the Fourier transforms of the two images. It results in a
|
||||
kernel, whose characteristic size is related to the exactness of the
|
||||
possible alignment between the two images, on top of a random background
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : array
|
||||
The first image, as a complex or real valued array
|
||||
im2 : array
|
||||
The first image, as a complex or real valued array
|
||||
im_slice : slice
|
||||
Default is from 3/8 to 5/8 across the image, a slice to use in the processing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
corr : array
|
||||
The complex-valued deconvolved cross-correlation, in real space
|
||||
|
||||
"""
|
||||
|
||||
im_np = False
|
||||
if isinstance(im1, np.ndarray):
|
||||
im1 = t.as_tensor(im1)
|
||||
im_np = True
|
||||
if isinstance(im2, np.ndarray):
|
||||
im2 = t.as_tensor(im2)
|
||||
im_np = True
|
||||
|
||||
if im_slice is None:
|
||||
im_slice = np.s_[(im1.shape[0]//8)*3:(im1.shape[0]//8)*5,
|
||||
(im1.shape[1]//8)*3:(im1.shape[1]//8)*5]
|
||||
|
||||
|
||||
cor_fft = t.fft.fft2(im1[im_slice]) * \
|
||||
t.conj(t.fft.fft2(im2[im_slice]))
|
||||
|
||||
# Not sure if this is more or less stable than just the correlation
|
||||
# maximum - requires some testing
|
||||
cor = t.fft.ifft2(cor_fft / t.abs(cor_fft))
|
||||
|
||||
if im_np:
|
||||
cor = cor.numpy()
|
||||
|
||||
return cor
|
||||
|
||||
|
||||
def calc_frc(im1, im2, basis, im_slice=None, nbins=None, snr=1., limit='side'):
|
||||
"""Calculates a Fourier ring correlation between two images
|
||||
|
||||
This function requires an input of a basis to allow for FRC calculations
|
||||
to be related to physical units.
|
||||
|
||||
Like other analysis functions, this can take input in numpy or pytorch,
|
||||
and will return output in the respective format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : array
|
||||
The first image, a complex or real valued array
|
||||
im2 : array
|
||||
The first image, a complex or real valued array
|
||||
basis : array
|
||||
The basis for the images, defined as is standard for datasets
|
||||
im_slice : slice
|
||||
Default is from 3/8 to 5/8 across the image, a slice to use in the processing.
|
||||
nbins : int
|
||||
Number of bins to break the FRC up into
|
||||
snr : float
|
||||
The signal to noise ratio (for the combined information in both images) to return a threshold curve for.
|
||||
limit : str
|
||||
Default is 'side'. What is the highest frequency to calculate the FRC to? If 'side', it chooses the side of the Fourier transform, if 'corner' it goes fully to the corner.
|
||||
|
||||
Returns
|
||||
-------
|
||||
freqs : array
|
||||
The frequencies associated with each FRC value
|
||||
FRC : array
|
||||
The FRC values
|
||||
threshold : array
|
||||
The threshold curve for comparison
|
||||
|
||||
"""
|
||||
|
||||
im_np = False
|
||||
if isinstance(im1, np.ndarray):
|
||||
im1 = t.as_tensor(im1)
|
||||
im_np = True
|
||||
if isinstance(im2, np.ndarray):
|
||||
im2 = t.as_tensor(im2)
|
||||
im_np = True
|
||||
|
||||
if isinstance(basis, np.ndarray):
|
||||
basis = t.tensor(basis)
|
||||
|
||||
if im_slice is None:
|
||||
im_slice = np.s_[(im1.shape[0]//8)*3:(im1.shape[0]//8)*5,
|
||||
(im1.shape[1]//8)*3:(im1.shape[1]//8)*5]
|
||||
|
||||
if nbins is None:
|
||||
nbins = np.max(im1[im_slice].shape) // 8
|
||||
|
||||
f1 = t.fft.fftshift(t.fft.fft2(im1[im_slice]),dim=(-1,-2))
|
||||
f2 = t.fft.fftshift(t.fft.fft2(im2[im_slice]),dim=(-1,-2))
|
||||
cor_fft = f1 * t.conj(f2)
|
||||
|
||||
F1 = t.abs(f1)**2
|
||||
F2 = t.abs(f2)**2
|
||||
|
||||
|
||||
di = np.linalg.norm(basis[:,0])
|
||||
dj = np.linalg.norm(basis[:,1])
|
||||
|
||||
i_freqs = fftpack.fftshift(fftpack.fftfreq(cor_fft.shape[0],d=di))
|
||||
j_freqs = fftpack.fftshift(fftpack.fftfreq(cor_fft.shape[1],d=dj))
|
||||
|
||||
Js,Is = np.meshgrid(j_freqs,i_freqs)
|
||||
Rs = np.sqrt(Is**2+Js**2)
|
||||
|
||||
if limit.lower().strip() == 'side':
|
||||
max_i = np.max(i_freqs)
|
||||
max_j = np.max(j_freqs)
|
||||
frc_range = [0, max(max_i,max_j)]
|
||||
elif limit.lower().strip() == 'corner':
|
||||
frc_range = [0, np.max(Rs)]
|
||||
else:
|
||||
raise ValueError('Invalid FRC limit: choose "side" or "corner"')
|
||||
|
||||
numerator, bins = np.histogram(Rs, bins=nbins, range=frc_range,
|
||||
weights=cor_fft.numpy())
|
||||
denominator_F1, bins = np.histogram(Rs, bins=nbins, range=frc_range,
|
||||
weights=F1.detach().cpu().numpy())
|
||||
denominator_F2, bins = np.histogram(Rs, bins=nbins, range=frc_range,
|
||||
weights=F2.detach().cpu().numpy())
|
||||
n_pix, bins = np.histogram(Rs, bins=nbins, range=frc_range)
|
||||
|
||||
|
||||
#n_pix = n_pix / 4 # This is for an apodized image, apodized with a hann window
|
||||
|
||||
frc = np.abs(numerator) / np.sqrt(denominator_F1*denominator_F2)
|
||||
|
||||
# This moves from combined-image SNR to single-image SNR
|
||||
snr /= 2
|
||||
|
||||
# NOTE: I should update this to produce lots of different threshold curves
|
||||
# sigma, 2sigma, 3sigma, traditional FRC 1-bit, my better one, n_pix, etc.
|
||||
|
||||
threshold = (snr + (2 * np.sqrt(snr) + 1) / np.sqrt(n_pix)) / \
|
||||
(1 + snr + (2 * np.sqrt(snr)) / np.sqrt(n_pix))
|
||||
|
||||
my_threshold = np.sqrt(snr**2 + (2*snr**2 + 2*snr + 1)/n_pix) / \
|
||||
np.sqrt(snr**2 + 2 * snr + 1 + 2*snr**2 / n_pix)
|
||||
|
||||
twosigma_threshold = 2/ np.sqrt(n_pix)
|
||||
|
||||
if not im_np:
|
||||
bins = t.tensor(bins)
|
||||
frc = t.tensor(frc)
|
||||
threshold = t.tensor(threshold)
|
||||
|
||||
return bins[:-1], frc, threshold
|
||||
|
||||
|
||||
def calc_vn_entropy(matrix):
|
||||
"""Calculates the Von Neumann entropy of a density matrix
|
||||
|
||||
Will either accept a single matrix, or a stack of matrices. Matrices
|
||||
are assumed to be Hermetian and positive definite, to be well-formed
|
||||
density matrices
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matrix : np.array
|
||||
The nxn matrix or lxnxn stack of matrices to calculate the entropy of
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
entropy: float or np.array
|
||||
The entropy or entropies of the arrays
|
||||
"""
|
||||
|
||||
if len(matrix.shape) == 3:
|
||||
# Get the eigenvalues
|
||||
eigs = [np.linalg.eigh(mat)[0] for mat in matrix]
|
||||
# Normalize them to match standard density matrix form
|
||||
eigs = [eig / np.sum(eig) for eig in eigs]
|
||||
# And calculate the VN entropy!
|
||||
entropies = [-np.sum(special.xlogy(eig,eig)) for eig in eigs]
|
||||
return np.array(entropies)
|
||||
else:
|
||||
eig = np.linalg.eigh(matrix)[0]
|
||||
entropy = -np.sum(special.xlogy(eig,eig))/np.sum(eig)
|
||||
return entropy
|
||||
|
||||
|
||||
def calc_top_mode_fraction(matrix):
|
||||
"""Calculates the fraction of total power in the top mode of a density matrix
|
||||
|
||||
Will either accept a single matrix, or a stack of matrices. Matrices
|
||||
are assumed to be Hermetian and positive definite, to be well-formed
|
||||
density matrices
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matrix : np.array
|
||||
The nxn matrix or lxnxn stack of matrices to work from
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
entropy: float or np.array
|
||||
The fraction of power in the top mode of each matrix
|
||||
"""
|
||||
|
||||
if len(matrix.shape) == 3:
|
||||
# Get the eigenvalues
|
||||
eigs = [np.linalg.eigh(mat)[0] for mat in matrix]
|
||||
# Normalize them to match standard density matrix form
|
||||
fractions = [np.max(eig) / np.sum(eig) for eig in eigs]
|
||||
return np.array(fractions)
|
||||
else:
|
||||
eig = np.linalg.eigh(matrix)[0]
|
||||
fraction = np.max(eig) / np.sum(eig)
|
||||
return fraction
|
||||
|
||||
|
||||
def calc_rms_error(field_1, field_2, align_phases=True, normalize=False,
|
||||
dims=2):
|
||||
"""Calculates the root-mean-squared error between two complex wavefields
|
||||
|
||||
The formal definition of this function is:
|
||||
|
||||
output = norm * sqrt(mean(abs(field_1 - gamma * field_2)**2))
|
||||
|
||||
Where norm is an optional normalization factor, and gamma is an
|
||||
optional phase factor which is appropriate when the wavefields suffer
|
||||
from a global phase degeneracy as is often the case in diffractive
|
||||
imaging.
|
||||
|
||||
The normalization is defined as the square root of the total intensity
|
||||
contained in field_1, which is appropriate when field_1 represents a
|
||||
known ground truth:
|
||||
|
||||
norm = sqrt(mean(abs(field_1)**2))
|
||||
|
||||
The phase offset is an analytic expression for the phase offset which
|
||||
will minimize the RMS error between the two wavefields:
|
||||
|
||||
gamma = exp(1j * angle(sum(field_1 * conj(field_2))))
|
||||
|
||||
This implementation is stable even in cases where field_1 and field_2
|
||||
are completely orthogonal.
|
||||
|
||||
In the definitions above, the field_n are n-dimensional wavefields. The
|
||||
dimensionality of the wavefields can be altered via the dims argument,
|
||||
but the default is 2 for a 2D wavefield.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
field_1 : array
|
||||
The first complex-valued field
|
||||
field_2 : array
|
||||
The second complex-valued field
|
||||
align_phases : bool
|
||||
Default is True, whether to account for a global phase offset
|
||||
normalize : bool
|
||||
Default is False, whether to normalize to the intensity of field_1
|
||||
dims : (int or tuple of python:ints)
|
||||
Default is 2, the number of final dimensions to reduce over.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
rms_error : float or t.Tensor
|
||||
The RMS error, or tensor of RMS errors, depending on the dim argument
|
||||
|
||||
"""
|
||||
|
||||
sumdims = tuple(d - dims for d in range(dims))
|
||||
|
||||
if align_phases:
|
||||
# Keepdim allows us to broadcast the result correctly when we
|
||||
# multiply by the fields
|
||||
gamma = t.exp(1j * t.angle(t.sum(field_1 * t.conj(field_2), dim=sumdims,
|
||||
keepdim=True)))
|
||||
else:
|
||||
gamma = 1
|
||||
|
||||
if normalize:
|
||||
norm = 1 / t.mean(t.abs(field_1)**2, dim=sumdims)
|
||||
else:
|
||||
norm = 1
|
||||
|
||||
difference = field_1 - gamma * field_2
|
||||
|
||||
return t.sqrt(norm * t.mean(t.abs(difference)**2, dim=sumdims))
|
||||
|
||||
|
||||
def calc_fidelity(fields_1, fields_2, dims=2):
|
||||
"""Calculates the fidelity between two density matrices
|
||||
|
||||
The fidelity is a comparison metric between two density matrices
|
||||
(i.e. mutual coherence functions) that extends the idea of the
|
||||
overlap to incoherent light. As a reminder, the overlap between two
|
||||
fields is:
|
||||
|
||||
overlap = abs(sum(field_1 * field_2))**2
|
||||
|
||||
Whereas the fidelity is defined as:
|
||||
|
||||
fidelity = trace(sqrt(sqrt(dm_1) <dot> dm_2 <dot> sqrt(dm_1)))**2
|
||||
|
||||
where dm_n refers to the density matrix encoded by fields_n such
|
||||
that dm_n = fields_n <dot> fields_<n>.conjtranspose(), sqrt
|
||||
refers to the matrix square root, and <dot> is the matrix product.
|
||||
|
||||
This is not a practical implementation, however, as it is not feasible
|
||||
to explicitly construct the matrices dm_1 and dm_2 in memory. Therefore,
|
||||
we take advantage of the alternate definition based directly on the
|
||||
fields_<n> parameter:
|
||||
|
||||
fidelity = sum(svdvals(fields_1 <dot> fields_2.conjtranspose()))**2
|
||||
|
||||
In the definitions above, the fields_n are regarded as collections of
|
||||
wavefields, where each wavefield is by default 2-dimensional. The
|
||||
dimensionality of the wavefields can be altered via the dims argument,
|
||||
but the fields_n arguments must always have at least one more dimension
|
||||
than the dims argument. Any additional dimensions are treated as batch
|
||||
dimensions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fields_1 : array
|
||||
The first set of complex-valued field modes
|
||||
fields_2 : array
|
||||
The second set of complex-valued field modes
|
||||
dims : int
|
||||
Default is 2, the number of final dimensions to reduce over.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
fidelity : float or t.Tensor
|
||||
The fidelity, or tensor of fidelities, depending on the dim argument
|
||||
|
||||
"""
|
||||
|
||||
fields_1 = t.as_tensor(fields_1)
|
||||
fields_2 = t.as_tensor(fields_2)
|
||||
|
||||
mult = fields_1.unsqueeze(-dims-2) * fields_2.unsqueeze(-dims-1).conj()
|
||||
sumdims = tuple(d - dims for d in range(dims))
|
||||
mat = t.sum(mult,dim=sumdims)
|
||||
|
||||
# Because I think this is the nuclear norm squared, I would like to swap
|
||||
# Out the definition for this, but I need to test it before swapping.
|
||||
# It also probably makes sense to implement sqrt_fidelity separately
|
||||
# because that's more important
|
||||
#return t.linalg.matrix_norm(mat, ord='nuc')**2
|
||||
|
||||
# I think this is just the nuclear norm.
|
||||
svdvals = t.linalg.svdvals(mat)
|
||||
return t.sum(svdvals, dim=-1)**2
|
||||
|
||||
|
||||
def calc_generalized_rms_error(fields_1, fields_2, normalize=False, dims=2):
|
||||
"""Calculates a generalization of the root-mean-squared error between two complex wavefields
|
||||
|
||||
This function calculates an generalization of the RMS error which uses the
|
||||
concept of fidelity to extend it to capture the error between
|
||||
incoherent wavefields, defined as a mode decomposition. The extension has
|
||||
several nice properties, in particular:
|
||||
|
||||
1) For coherent wavefields, it precisely matches the RMS error including
|
||||
a correction for the global phase degeneracy (align_phases=True)
|
||||
2) All mode decompositions of either field that correspond to the same
|
||||
density matrix / mutual coherence function will produce the same
|
||||
output
|
||||
3) The error will only be zero when comparing mode decompositions that
|
||||
correspond to the same density matrix.
|
||||
4) Due to (2), one need not worry about the ordering of the modes,
|
||||
properly orthogonalizing the modes, and it is even possible to
|
||||
compare mode decompositions with different numbers of modes.
|
||||
|
||||
The formal definition of this function is:
|
||||
|
||||
output = norm * sqrt(mean(abs(fields_1)**2)
|
||||
+ mean(abs(fields_2)**2)
|
||||
- 2 * sqrt(fidelity(fields_1,fields_2)))
|
||||
|
||||
Where norm is an optional normalization factor, and the fidelity is
|
||||
defined based on the mean, rather than the sum, to match the convention
|
||||
for the root *mean* squared error.
|
||||
|
||||
The normalization is defined as the square root of the total intensity
|
||||
contained in fields_1, which is appropriate when fields_1 represents a
|
||||
known ground truth:
|
||||
|
||||
norm = sqrt(mean(abs(fields_1)**2))
|
||||
|
||||
In the definitions above, the fields_n are regarded as collections of
|
||||
wavefields, where each wavefield is by default 2-dimensional. The
|
||||
dimensionality of the wavefields can be altered via the dims argument,
|
||||
but the fields_n arguments must always have at least one more dimension
|
||||
than the dims argument. Any additional dimensions are treated as batch
|
||||
dimensions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fields_1 : array
|
||||
The first set of complex-valued field modes
|
||||
fields_2 : array
|
||||
The second set of complex-valued field modes
|
||||
normalize : bool
|
||||
Default is False, whether to normalize to the intensity of fields_1
|
||||
dims : (int or tuple of python:ints)
|
||||
Default is 2, the number of final dimensions to reduce over.
|
||||
|
||||
Returns
|
||||
-------
|
||||
rms_error : float or t.Tensor
|
||||
The generalized RMS error, or tensor of generalized RMS errors, depending on the dim argument
|
||||
|
||||
"""
|
||||
fields_1 = t.as_tensor(fields_1)
|
||||
fields_2 = t.as_tensor(fields_2)
|
||||
|
||||
npix = t.prod(t.as_tensor(fields_1.shape[-dims:],dtype=t.int32))
|
||||
|
||||
sumdims = tuple(d - dims - 1 for d in range(dims+1))
|
||||
fields_1_intensity = t.sum(t.abs(fields_1)**2,dim=sumdims) / npix
|
||||
fields_2_intensity = t.sum(t.abs(fields_2)**2,dim=sumdims) / npix
|
||||
fidelity = calc_fidelity(fields_1, fields_2, dims=dims) / npix**2
|
||||
|
||||
result = fields_1_intensity + fields_2_intensity - 2 * t.sqrt(fidelity)
|
||||
|
||||
if normalize:
|
||||
result /= fields_1_intensity
|
||||
|
||||
return t.sqrt(result)
|
||||
|
||||
|
||||
def calc_generalized_frc(fields_1, fields_2, basis, im_slice=None, nbins=None, snr=1.):
|
||||
"""Calculates a Fourier ring correlation between two images
|
||||
|
||||
This function requires an input of a basis to allow for FRC calculations
|
||||
to be related to physical units.
|
||||
|
||||
Like other analysis functions, this can take input in numpy or pytorch,
|
||||
and will return output in the respective format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : array
|
||||
The first image, a complex or real valued array
|
||||
im2 : array
|
||||
The first image, a complex or real valued array
|
||||
basis : array
|
||||
The basis for the images, defined as is standard for datasets
|
||||
im_slice : slice
|
||||
Default is from 3/8 to 5/8 across the image, a slice to use in the processing.
|
||||
nbins : int
|
||||
Number of bins to break the FRC up into
|
||||
snr : float
|
||||
The signal to noise ratio (for the combined information in both images) to return a threshold curve for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
freqs : array
|
||||
The frequencies associated with each FRC value
|
||||
FRC : array
|
||||
The FRC values
|
||||
threshold : array
|
||||
The threshold curve for comparison
|
||||
|
||||
"""
|
||||
|
||||
im_np = False
|
||||
if isinstance(fields_1, np.ndarray):
|
||||
fields_1 = t.as_tensor(fields_)
|
||||
im_np = True
|
||||
if isinstance(fields_2, np.ndarray):
|
||||
fields_2 = t.as_tensor(fields_2)
|
||||
im_np = True
|
||||
|
||||
if isinstance(basis, np.ndarray):
|
||||
basis = t.tensor(basis)
|
||||
|
||||
if im_slice is None:
|
||||
im_slice = np.s_[(im1.shape[0]//8)*3:(im1.shape[0]//8)*5,
|
||||
(im1.shape[1]//8)*3:(im1.shape[1]//8)*5]
|
||||
|
||||
if nbins is None:
|
||||
nbins = np.max(fields_1[...,im_slice].shape[-2:]) // 4
|
||||
|
||||
|
||||
f1 = t.fft.fftshift(t.fft.fft2(im1[im_slice]),dim=(-1,-2))
|
||||
f2 = t.fft.fftshift(t.fft.fft2(im2[im_slice]),dim=(-1,-2))
|
||||
cor_fft = f1 * t.conj(f2)
|
||||
|
||||
|
||||
F1 = t.abs(f1)**2
|
||||
F2 = t.abs(f2)**2
|
||||
|
||||
|
||||
di = np.linalg.norm(basis[:,0])
|
||||
dj = np.linalg.norm(basis[:,1])
|
||||
|
||||
i_freqs = fftpack.fftshift(fftpack.fftfreq(cor_fft.shape[0],d=di))
|
||||
j_freqs = fftpack.fftshift(fftpack.fftfreq(cor_fft.shape[1],d=dj))
|
||||
|
||||
Js,Is = np.meshgrid(j_freqs,i_freqs)
|
||||
Rs = np.sqrt(Is**2+Js**2)
|
||||
|
||||
# This line is used to get a set of bins that matches the logic
|
||||
# used by np.histogram, so that this function will match the choices
|
||||
# of bin edges that comes from the non-generalized version. This also
|
||||
# gets us the count on the number of pixels per bin so we can calculate
|
||||
# the threshold curve
|
||||
n_pix, bins = np.histogram(Rs,bins=nbins)
|
||||
|
||||
frc = []
|
||||
for i in range(len(bins)-1):
|
||||
mask = t.logical_and(Rs<bins[i+1], Rs>=bins[i])
|
||||
masked_f1 = f1 * mask[...,:,:]
|
||||
masked_f2 = f2 * mask[...,:,:]
|
||||
numerator = t.sqrt(calc_fidelity(masked_f1, masked_f2))
|
||||
denominator_f1 = t.sqrt(calc_fidelity(masked_f1, masked_f1))
|
||||
denominator_f2 = t.sqrt(calc_fidelity(masked_f2, masked_f2))
|
||||
frc.append(numerator / t.sqrt((denominator_f1 * denominator_f2)))
|
||||
|
||||
frc = np.array(frc)
|
||||
|
||||
# This moves from combined-image SNR to single-image SNR
|
||||
snr /= 2
|
||||
|
||||
threshold = (snr + (2 * snr + 1) / np.sqrt(n_pix)) / \
|
||||
(1 + snr + (2 * np.sqrt(snr)) / np.sqrt(n_pix))
|
||||
|
||||
if not im_np:
|
||||
bins = t.tensor(bins)
|
||||
frc = t.tensor(frc)
|
||||
threshold = t.tensor(threshold)
|
||||
|
||||
return bins[:-1], frc, threshold
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.atoms.atoms import *
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
from CDTools.tools.data.data import *
|
||||
@@ -0,0 +1,781 @@
|
||||
"""Contains the base functions for loading and saving data from/to .cxi files
|
||||
|
||||
These functions are used when constructing a new dataset class to pull
|
||||
specific desired information from a .cxi file. These functions should
|
||||
handle all the needed conversions between standard formats (for example,
|
||||
transposes of the basis arrays, shifting from object to probe motion, etc).
|
||||
"""
|
||||
|
||||
import h5py
|
||||
import numpy as np
|
||||
import numbers
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import torch as t
|
||||
from contextlib import contextmanager
|
||||
|
||||
__all__ = ['get_entry_info',
|
||||
'get_sample_info',
|
||||
'get_wavelength',
|
||||
'get_detector_geometry',
|
||||
'get_mask',
|
||||
'get_dark',
|
||||
'get_data',
|
||||
'get_shot_to_shot_info',
|
||||
'get_ptycho_translations',
|
||||
'create_cxi',
|
||||
'add_entry_info',
|
||||
'add_sample_info',
|
||||
'add_source',
|
||||
'add_detector',
|
||||
'add_mask',
|
||||
'add_dark',
|
||||
'add_data',
|
||||
'add_shot_to_shot_info',
|
||||
'add_ptycho_translations']
|
||||
|
||||
|
||||
#
|
||||
# Functions to inspect the basic attributes of a cxi file represented as an
|
||||
# h5 file object
|
||||
#
|
||||
|
||||
|
||||
def get_entry_info(cxi_file):
|
||||
"""Returns a dictionary with the basic metadata from the cxi file's entry_1 attribute
|
||||
|
||||
String type metadata is read out as a string, and datetime metadata
|
||||
is converted to python datetime objects if the string is properly
|
||||
formatted.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
entry_info : dict
|
||||
A dictionary with basic metadata defined in the cxi file
|
||||
|
||||
"""
|
||||
e1 = cxi_file['entry_1']
|
||||
metadata_attrs = ['title',
|
||||
'experiment_identifier',
|
||||
'experiment_description',
|
||||
'program_name',
|
||||
'start_time',
|
||||
'end_time']
|
||||
metadata = {attr: str(e1[attr][()].decode()) for attr in metadata_attrs
|
||||
if attr in e1}
|
||||
|
||||
|
||||
for attr in metadata_attrs:
|
||||
if attr in e1:
|
||||
try:
|
||||
metadata[attr] = dateutil.parser.parse(str(e1[attr][()].decode()))
|
||||
except ValueError:
|
||||
metadata[attr] = str(e1[attr][()].decode())
|
||||
return metadata
|
||||
|
||||
|
||||
def get_sample_info(cxi_file):
|
||||
"""Returns a dictionary with the basic metadata from the cxi file's entry_1/sample_1 attribute
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
sample_info : dict
|
||||
A dictionary with basic metadata from the sample defined in the cxi file
|
||||
|
||||
"""
|
||||
if 'entry_1/sample_1' not in cxi_file:
|
||||
return None
|
||||
|
||||
s1 = cxi_file['entry_1/sample_1']
|
||||
metadata_attrs = ['name','description','unit_cell_group']
|
||||
|
||||
metadata = {}
|
||||
for attr in metadata_attrs:
|
||||
# Somehow different ways of saving can lead to different ways to
|
||||
# decode it here, so we try both
|
||||
if attr in s1:
|
||||
try:
|
||||
metadata[attr] = str(s1[attr][()].decode())
|
||||
except AttributeError as e:
|
||||
metadata[attr] = str(np.array(s1[attr][:])[0].decode())
|
||||
|
||||
float_attrs = ['concentration',
|
||||
'mass',
|
||||
'temperature',
|
||||
'thickness',
|
||||
'unit_cell_volume']
|
||||
|
||||
for attr in float_attrs:
|
||||
if attr in s1:
|
||||
metadata[attr] = np.float32(s1[attr][()])
|
||||
|
||||
if 'unit_cell' in s1:
|
||||
metadata['unit_cell'] = np.array(s1['unit_cell']).astype(np.float32)
|
||||
|
||||
|
||||
if 'geometry_1/orientation' in s1:
|
||||
orient = np.array(s1['geometry_1/orientation']).astype(np.float32)
|
||||
xvec = orient[:3] / np.linalg.norm(orient[:3])
|
||||
yvec = orient[3:] / np.linalg.norm(orient[3:])
|
||||
metadata['orientation'] = np.array([xvec,yvec,
|
||||
np.cross(xvec,yvec)])
|
||||
|
||||
if 'geometry_1/surface_normal' in s1:
|
||||
snorm = np.array(s1['geometry_1/surface_normal']).astype(np.float32)
|
||||
xvec = np.cross(np.array([0.,1.,0.]), snorm)
|
||||
xvec /= np.linalg.norm(xvec)
|
||||
yvec = np.cross(snorm, xvec)
|
||||
yvec /= np.linalg.norm(yvec)
|
||||
metadata['orientation'] = np.array([xvec, yvec, snorm])
|
||||
|
||||
# Check if the metadata is empty
|
||||
if metadata == {}:
|
||||
metadata = None
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def get_wavelength(cxi_file):
|
||||
"""Returns the wavelength of the source defined in the cxi file object, in m
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
wavelength: np.float32
|
||||
The wavelength of the source defined in the cxi file
|
||||
"""
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'source_1/wavelength' in i1:
|
||||
wavelength = np.float32(i1['source_1/wavelength'])
|
||||
elif 'source_1/energy' in i1:
|
||||
energy = np.float32(i1['source_1/energy'])
|
||||
wavelength = 1.9864459e-25 / energy
|
||||
else:
|
||||
raise KeyError('Neither Wavelength or Energy Defined in provided .cxi File')
|
||||
|
||||
return wavelength
|
||||
|
||||
|
||||
def get_detector_geometry(cxi_file):
|
||||
"""Returns a standardized description of the detector geometry defined in the cxi file object
|
||||
|
||||
It makes intelligent assumptions based on the definitions in the cxi
|
||||
file definition. The standardized description of the geometry that it
|
||||
outputs includes the sample to detector distance, the corner location
|
||||
of the detector, and the basis vectors defining the detector. It can
|
||||
only handle detectors defined as rectangular grids of pixels.
|
||||
|
||||
The distance and corner_location values are technically overdetermining
|
||||
the detector location, but for many experiments (particularly
|
||||
transmission experiments), the distance is needed and the exact
|
||||
corner location is not. If the corner location is not reported in
|
||||
the cxi file, no attempt will be made to calculate it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
distance : np.float32
|
||||
The sample to detector distance, in m
|
||||
basis_vectors : np.array
|
||||
The basis vectors for the detector
|
||||
corner_location : np.array
|
||||
The real-space location of the (0,0) pixel in the detector
|
||||
|
||||
"""
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
d1 = i1['detector_1']
|
||||
|
||||
if 'detector_1/basis_vectors' in i1:
|
||||
basis_vectors = np.array(d1['basis_vectors'])
|
||||
if basis_vectors.shape == (2,3):
|
||||
basis_vectors = basis_vectors.T
|
||||
else:
|
||||
# This whole thing just to account for all the ways people can
|
||||
# implicitly define the x or y pixel size for a detector. I've
|
||||
# seen too many of these in the wild, unfortunately...
|
||||
try:
|
||||
x_pixel_size = np.float32(d1['x_pixel_size'])
|
||||
except:
|
||||
x_pixel_size = None
|
||||
try:
|
||||
y_pixel_size = np.float32(d1['y_pixel_size'])
|
||||
except:
|
||||
y_pixel_size = None
|
||||
|
||||
if x_pixel_size is None and y_pixel_size is not None:
|
||||
x_pixel_size = y_pixel_size
|
||||
elif x_pixel_size is not None and y_pixel_size is None:
|
||||
y_pixel_size = x_pixel_size
|
||||
if x_pixel_size is None and y_pixel_size is None:
|
||||
raise KeyError('Detector pixel size not defined in file.')
|
||||
basis_vectors = np.array([[0,-y_pixel_size,0],
|
||||
[-x_pixel_size,0,0]]).transpose()
|
||||
|
||||
try:
|
||||
distance = np.float32(d1['distance'])
|
||||
except:
|
||||
distance = None
|
||||
try:
|
||||
corner_position = np.array(d1['corner_position'])
|
||||
except:
|
||||
corner_position = None
|
||||
|
||||
# Don't pretend to calculate corner position from distance if it's not
|
||||
# defined, but do calculate distance from corner position if distance is
|
||||
# not defined. If neither is defined, then raise an error.
|
||||
if distance is None and corner_position is not None:
|
||||
detector_normal = np.cross(basis_vectors[:,0],
|
||||
basis_vectors[:,1])
|
||||
detector_normal /= np.linalg.norm(detector_normal)
|
||||
distance = np.linalg.norm(np.dot(corner_position, detector_normal))
|
||||
|
||||
if distance is None and corner_position is None:
|
||||
raise KeyError('Neither sample to detector distance nor corner position is defined in file.')
|
||||
|
||||
return distance, basis_vectors, corner_position
|
||||
|
||||
|
||||
def get_mask(cxi_file):
|
||||
"""Returns the detector mask defined in the cxi file object
|
||||
|
||||
This function converts from the format specified in the cxi file
|
||||
definition to a simple on/off mask, where a value of 1 defines a
|
||||
good pixel (on) and a value of 0 defines a bad pixel (off).
|
||||
|
||||
If any bit is set in the mask at all, it will be defined as a bad
|
||||
pixel, with the exception of pixels marked exactly as 0x00001000,
|
||||
which is defined to mean that the pixel has signal above the
|
||||
background. These pixels are treated as on pixels.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
mask : np.array
|
||||
An array storing the mask from the cxi file
|
||||
"""
|
||||
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1/mask' in i1:
|
||||
mask = np.array(i1['detector_1/mask']).astype(np.uint32)
|
||||
mask_on = np.equal(mask,np.uint32(0))
|
||||
mask_has_signal = np.equal(mask,np.uint32(0x00001000))
|
||||
return np.logical_or(mask_on,mask_has_signal).astype(bool)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_dark(cxi_file):
|
||||
"""Returns an array with a dark image to use for initialization of a background model
|
||||
|
||||
This looks for a set of dark images at
|
||||
entry_1/instrument_1/detector_1/data_dark. If the darks exist, it will
|
||||
return the mean of the array along all axes but the last two. That is,
|
||||
if the dark image is a single image, it will return that image. If it
|
||||
is a stack of images, it will return the mean along the stack axis.
|
||||
|
||||
If the darks do not exist, it will return None.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
dark : np.array
|
||||
An array storing the dark image
|
||||
"""
|
||||
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1/data_dark' in i1:
|
||||
darks = np.array(i1['detector_1/data_dark'])
|
||||
dims = tuple(range(len(darks.shape) - 2))
|
||||
darks = np.nanmean(darks,axis=dims)
|
||||
else:
|
||||
darks = None
|
||||
|
||||
return darks
|
||||
|
||||
|
||||
def get_data(cxi_file, cut_zeros = True):
|
||||
"""Returns an array with the full stack of detector data defined in the cxi file object
|
||||
|
||||
This function will make sure to check all the various places that it's
|
||||
okay to store the data in, to ensure that it can find the data regardless
|
||||
of whether the creator of the .cxi file has remembered to link the data
|
||||
to all the required locations.
|
||||
|
||||
It will return the data array in whatever shape it's defined in.
|
||||
|
||||
It will also read out the axes attribute of the data into a list of strings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
cut_zeros : bool
|
||||
Default True, whether to set all negative data to zero
|
||||
|
||||
Returns
|
||||
-------
|
||||
data : np.array
|
||||
An array storing the data defined in the cxi file
|
||||
axes : list(str)
|
||||
A list of the axes defined in the axes attribute, if any
|
||||
"""
|
||||
|
||||
# Possible locations for the data
|
||||
if 'entry_1/data_1/data' in cxi_file:
|
||||
pull_from = 'entry_1/data_1/data'
|
||||
elif 'entry_1/instrument_1/detector_1/data' in cxi_file:
|
||||
pull_from = 'entry_1/instrument_1/detector_1/data'
|
||||
else:
|
||||
raise KeyError('Data is not defined within cxi file')
|
||||
|
||||
data = cxi_file[pull_from][:]
|
||||
# Use maximum in-place to avoid allocating any more memory than is needed
|
||||
if cut_zeros:
|
||||
np.maximum(data,0,data)
|
||||
|
||||
if 'axes' in cxi_file[pull_from].attrs:
|
||||
try:
|
||||
axes = str(cxi_file[pull_from].attrs['axes'].decode()).split(':')
|
||||
except AttributeError as e: # Weird string vs bytes thing, ehhh
|
||||
axes = str(cxi_file[pull_from].attrs['axes']).split(':')
|
||||
|
||||
axes = [axis.strip().lower() for axis in axes]
|
||||
else:
|
||||
axes = None
|
||||
|
||||
return data, axes
|
||||
|
||||
|
||||
def get_shot_to_shot_info(cxi_file, field_name):
|
||||
"""Gets a specified dataset of shot-to-shot information from the cxi file
|
||||
|
||||
The data is assumed to be in the form of an array, with one dimension
|
||||
being the number of patterns being stored in the dataset. This is
|
||||
helpful for storing additional readback data on the shot-to-shot
|
||||
level that may be important but doens't have a clearly defined
|
||||
place to be stored in the .cxi file specification. Such data includes
|
||||
shot-to-shot probe intensity measurements, polarizer positions, etc.
|
||||
|
||||
It will look for this data in 3 places (in the following order):
|
||||
|
||||
1) entry_1/data_1/<field_name>
|
||||
2) entry_1/sample_1/geometry_1/<field_name>
|
||||
3) entry_1/instrument_1/detector_1/<field_name>
|
||||
|
||||
This function is also used internally to read out the translations
|
||||
associated with a ptychography experiment
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
field_name : str
|
||||
The name of the field to be read from
|
||||
|
||||
Returns
|
||||
-------
|
||||
data : np.array
|
||||
An array storing the translations defined in the cxi file
|
||||
"""
|
||||
if 'entry_1/data_1/' + field_name in cxi_file:
|
||||
pull_from = 'entry_1/data_1/' + field_name
|
||||
elif 'entry_1/sample_1/geometry_1/' + field_name in cxi_file:
|
||||
pull_from = 'entry_1/sample_1/geometry_1/' + field_name
|
||||
elif 'entry_1/instrument_1/detector_1/' in cxi_file:
|
||||
pull_from = 'entry_1/instrument_1/detector_1/' + field_name
|
||||
else:
|
||||
raise KeyError('Data is not defined within cxi file')
|
||||
|
||||
return np.array(cxi_file[pull_from]).astype(np.float32)
|
||||
|
||||
|
||||
def get_ptycho_translations(cxi_file):
|
||||
"""Gets an array of x,y,z translations, if such an array has been defined in the file
|
||||
|
||||
It negates the translations, because the CXI file format is designed
|
||||
to specify translations of the samples and the CDTools code specifies
|
||||
translations of the optics.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
A file object to be read
|
||||
|
||||
Returns
|
||||
-------
|
||||
translations : np.array
|
||||
An array storing the translations defined in the cxi file
|
||||
"""
|
||||
|
||||
translations = get_shot_to_shot_info(cxi_file, 'translation')
|
||||
return -translations
|
||||
|
||||
|
||||
#
|
||||
# Now we move on to the helper functions for writing CXI files
|
||||
#
|
||||
|
||||
|
||||
def create_cxi(filename):
|
||||
"""Creates a new cxi file with a single entry group
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : str
|
||||
The path at which to create the file
|
||||
"""
|
||||
file_obj = h5py.File(filename,'w')
|
||||
file_obj.create_dataset('cxi_version', data=160)
|
||||
file_obj.create_dataset('number_of_entries',data=1)
|
||||
e1f = file_obj.create_group('entry_1')
|
||||
return file_obj
|
||||
|
||||
|
||||
def add_entry_info(cxi_file, metadata):
|
||||
"""Adds a dictionary of entry metadata to the entry_1 group of a cxi file object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the info to
|
||||
metadata : dict
|
||||
A dictionary containing all the metadata to be stored
|
||||
"""
|
||||
# Just the string and datetime types should be relevant but all are
|
||||
# included in case the cxi spec becomes more permissive
|
||||
for key, value in metadata.items():
|
||||
if isinstance(value,(str,bytes)):
|
||||
cxi_file['entry_1'][key] = np.string_(value)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
cxi_file['entry_1'][key] = np.string_(value.isoformat())
|
||||
elif isinstance(value, numbers.Number):
|
||||
cxi_file['entry_1'][key] = value
|
||||
elif isinstance(value, (np.ndarray,list,tuple)):
|
||||
cxi_file['entry_1'].create_dataset(key, data=np.asarray(value))
|
||||
elif isinstance(value, t.Tensor):
|
||||
asnumpy = value.detach().cpu().numpy()
|
||||
cxi_file['entry_1'].create_dataset(key, data=asnumpy)
|
||||
|
||||
|
||||
|
||||
def add_sample_info(cxi_file, metadata):
|
||||
"""Adds a dictionary of entry metadata to the entry_1/sample_1 group of a cxi file object
|
||||
|
||||
This function will create the sample_1 attribute if it doesn't already exist
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the info to
|
||||
metadata : dict
|
||||
A dictionary containing all the metadata to be stored
|
||||
"""
|
||||
if 'entry_1/sample_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('sample_1')
|
||||
s1 = cxi_file['entry_1/sample_1']
|
||||
|
||||
if 'orientation' in metadata:
|
||||
if 'geometry_1' not in s1:
|
||||
s1.create_group('geometry_1')
|
||||
# Only store the part of this matrix as defined in the CXI file spec
|
||||
s1['geometry_1'].create_dataset('orientation',
|
||||
data=metadata['orientation'].ravel()[:6])
|
||||
|
||||
for key, value in metadata.items():
|
||||
if key == 'orientation':
|
||||
continue # this is a special case
|
||||
if isinstance(value,(str,bytes)):
|
||||
s1[key] = np.string_(value)
|
||||
elif isinstance(value, datetime.datetime):
|
||||
s1[key] = np.string_(value.isoformat())
|
||||
elif isinstance(value, numbers.Number):
|
||||
s1[key] = value
|
||||
elif isinstance(value, (np.ndarray,list,tuple)):
|
||||
s1.create_dataset(key, data=np.asarray(value))
|
||||
elif isinstance(value, t.Tensor):
|
||||
asnumpy = value.detach().cpu().numpy()
|
||||
s1.create_dataset(key, data=asnumpy)
|
||||
|
||||
|
||||
def add_source(cxi_file, wavelength):
|
||||
"""Adds the entry_1/source_1 group to a cxi file object
|
||||
|
||||
It stores the energy and wavelength attributes in the source_1 group,
|
||||
given a wavelength to define them from.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the source to
|
||||
wavelength : float
|
||||
The wavelength of light
|
||||
"""
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'source_1' not in i1:
|
||||
i1.create_group('source_1')
|
||||
s1 = i1['source_1']
|
||||
s1['wavelength'] = np.float32(wavelength)
|
||||
s1['energy'] = np.float32(1.9864459e-25 / wavelength)
|
||||
|
||||
|
||||
|
||||
def add_detector(cxi_file, distance, basis, corner=None):
|
||||
"""Adds the entry_1/instrument_1/detector_1 group to a cxi file object
|
||||
|
||||
It will define all the relevant parameters - distance, pixel size,
|
||||
detector basis, and corner position (if relevant) based on the provided
|
||||
information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the detector to
|
||||
distance : float
|
||||
The sample to detector distance
|
||||
basis : array
|
||||
The detector basis
|
||||
corner : array
|
||||
Optional, the corner position of the detector
|
||||
|
||||
"""
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1' not in i1:
|
||||
i1.create_group('detector_1')
|
||||
d1 = i1['detector_1']
|
||||
|
||||
d1['distance'] = np.float32(distance)
|
||||
|
||||
if isinstance(basis, t.Tensor):
|
||||
basis = basis.detach().cpu().numpy()
|
||||
if basis.shape == (2,3):
|
||||
basis = basis.T
|
||||
d1['x_pixel_size'] = np.linalg.norm(basis[:,1])
|
||||
d1['y_pixel_size'] = np.linalg.norm(basis[:,0])
|
||||
d1.create_dataset('basis_vectors', data=basis)
|
||||
|
||||
if corner is not None:
|
||||
if isinstance(corner, t.Tensor):
|
||||
corner = corner.detach().cpu().numpy()
|
||||
d1.create_dataset('corner_position',data=corner)
|
||||
|
||||
|
||||
def add_mask(cxi_file, mask):
|
||||
"""Adds the specified mask to the cxi file
|
||||
|
||||
It places the mask into the mask dataset under
|
||||
entry_1/instrument_1/detector_1. The internal mask is defined
|
||||
simply as a 1 for an "on" pixel and a 0 for an "off" pixel, and
|
||||
the saved mask is exactly the opposite. This is simpler than the
|
||||
most general mask allowed by the cxi file format but it captures the
|
||||
distinction between pixels to be used and pixels not to be used.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the mask to
|
||||
mask : array
|
||||
The mask to save out to the file
|
||||
"""
|
||||
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1' not in i1:
|
||||
i1.create_group('detector_1')
|
||||
d1 = i1['detector_1']
|
||||
if isinstance(mask, t.Tensor):
|
||||
mask = mask.detach().cpu().numpy()
|
||||
|
||||
mask_to_save = np.zeros(mask.shape).astype(np.uint32)
|
||||
mask_to_save[mask == 0] = 1
|
||||
d1.create_dataset('mask',data=mask_to_save)
|
||||
|
||||
|
||||
def add_dark(cxi_file, dark):
|
||||
"""Adds the specified dark image to a cxi file
|
||||
|
||||
It places the dark image data into the data_dark dataset under
|
||||
entry_1/instrument_1/detector_1.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the mask to
|
||||
dark : array
|
||||
The dark image(s) to save out to the file
|
||||
"""
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1' not in i1:
|
||||
i1.create_group('detector_1')
|
||||
d1 = i1['detector_1']
|
||||
if isinstance(dark, t.Tensor):
|
||||
dark = dark.detach().cpu().numpy()
|
||||
|
||||
d1.create_dataset('data_dark',data=dark)
|
||||
|
||||
|
||||
def add_data(cxi_file, data, axes=None, compression='gzip',
|
||||
chunks=True):
|
||||
"""Adds the specified data to the cxi file
|
||||
|
||||
It will add the data unchanged to the file, placing it in two spots:
|
||||
|
||||
1) The entry_1/instrument_1/detector_1/data path
|
||||
2) A softlink at entry_1/data_1/data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the data to
|
||||
data : array
|
||||
The data to be saved
|
||||
axes : list(str)
|
||||
Optional, a list of axis names to be saved in the axes attribute
|
||||
"""
|
||||
if 'entry_1/data_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('data_1')
|
||||
data1 = cxi_file['entry_1/data_1']
|
||||
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1' not in i1:
|
||||
i1.create_group('detector_1')
|
||||
det1 = i1['detector_1']
|
||||
|
||||
if isinstance(data, t.Tensor):
|
||||
data = data.detach().cpu().numpy()
|
||||
|
||||
det1.create_dataset('data', data=data, compression=compression,
|
||||
chunks=chunks)
|
||||
data1['data'] = h5py.SoftLink('/entry_1/instrument_1/detector_1/data')
|
||||
|
||||
if axes is not None:
|
||||
if isinstance(axes, list):
|
||||
axes_str = ':'.join(axes)
|
||||
else:
|
||||
axes_str = str(axes)
|
||||
det1['data'].attrs['axes'] = np.string_(axes_str)
|
||||
|
||||
|
||||
def add_shot_to_shot_info(cxi_file, data, field_name):
|
||||
"""Adds a specified dataset of shot-to-shot information to the cxi file
|
||||
|
||||
The data is assumed to be in the form of an array, with one dimension
|
||||
being the number of patterns being stored in the dataset. This is
|
||||
helpful for storing additional readback data on the shot-to-shot
|
||||
level that may be important but doens't have a clearly defined
|
||||
place to be stored in the .cxi file specification. Such data includes
|
||||
shot-to-shot probe intensity measurements, polarizer positions, etc.
|
||||
|
||||
This function is also used internally to store the translations
|
||||
associated with a ptychography experiment
|
||||
|
||||
It will store this data in 3 places:
|
||||
|
||||
1) The entry_1/sample_1/geometry_1/<field_name> path
|
||||
2) A softlink at entry_1/data_1/<field_name>
|
||||
3) A softlink at entry_1/instrument_1/detector_1/<field_name>
|
||||
|
||||
The geometry and detector paths may not always be relevant, but this
|
||||
ensures that the data is always available in any of the places that
|
||||
an eventual reader may go to look for, e.g., the translations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the translations to
|
||||
data : array
|
||||
The data to be saved
|
||||
field_name : str
|
||||
The field name to save the data under
|
||||
"""
|
||||
|
||||
if 'entry_1/sample_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('sample_1')
|
||||
s1 = cxi_file['entry_1/sample_1']
|
||||
|
||||
if 'geometry_1' not in s1:
|
||||
s1.create_group('geometry_1')
|
||||
g1 = s1['geometry_1']
|
||||
|
||||
if 'entry_1/data_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('data_1')
|
||||
data1 = cxi_file['entry_1/data_1']
|
||||
|
||||
if 'entry_1/instrument_1' not in cxi_file:
|
||||
cxi_file['entry_1'].create_group('instrument_1')
|
||||
i1 = cxi_file['entry_1/instrument_1']
|
||||
if 'detector_1' not in i1:
|
||||
i1.create_group('detector_1')
|
||||
det1 = i1['detector_1']
|
||||
|
||||
|
||||
if isinstance(data, t.Tensor):
|
||||
data = data.detach().cpu().numpy()
|
||||
|
||||
|
||||
g1.create_dataset(field_name, data=data)
|
||||
data1[field_name] = h5py.SoftLink('/entry_1/sample_1/geometry_1/'
|
||||
+ field_name)
|
||||
det1[field_name] = h5py.SoftLink('/entry_1/sample_1/geometry_1/'
|
||||
+ field_name)
|
||||
|
||||
|
||||
def add_ptycho_translations(cxi_file, translations):
|
||||
"""Adds the specified translations to the cxi file
|
||||
|
||||
It will add the translations to the file, negating them to conform to
|
||||
the standard in cxi files that the translations refer to the object's
|
||||
translation.
|
||||
|
||||
It will store them in 3 places:
|
||||
|
||||
1) The entry_1/sample_1/geometry_1/translation path
|
||||
2) A softlink at entry_1/data_1/translation
|
||||
3) A softlink at entry_1/instrument_1/detector_1/translation
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cxi_file : h5py.File
|
||||
The file to add the translations to
|
||||
translations : array
|
||||
The translations to be saved
|
||||
"""
|
||||
# accounting for the different definition between cxi files and
|
||||
# CDTools
|
||||
translations = -translations
|
||||
|
||||
add_shot_to_shot_info(cxi_file, translations, 'translation')
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.image_processing.image_processing import *
|
||||
@@ -0,0 +1,319 @@
|
||||
"""Contains functions for basic image processing needs
|
||||
|
||||
This module contains two kinds of image processing tools. The first type
|
||||
is specific tools for calculating commonly needed metrics (such as the
|
||||
centroid of an image), directly on complex-valued torch tensors. The second
|
||||
kind of tools perform common image manipulations on torch tensors, in such
|
||||
a way that it is safe to include them in automatic differentiation models.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import torch as t
|
||||
from CDTools.tools import propagators
|
||||
|
||||
__all__ = ['centroid', 'centroid_sq', 'sinc_subpixel_shift',
|
||||
'find_subpixel_shift', 'find_pixel_shift', 'find_shift',
|
||||
'convolve_1d', 'fourier_upsample']
|
||||
|
||||
|
||||
def centroid(im, dims=2):
|
||||
"""Returns the centroid of an image or a stack of images
|
||||
|
||||
By default, the last two dimensions are used in the calculation
|
||||
and the remainder of the dimensions are passed through.
|
||||
|
||||
Beware that the meaning of the centroid is not well defined if your
|
||||
image contains values less than 0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : torch.Tensor
|
||||
An image or stack of images to calculate from
|
||||
dims : int
|
||||
Default 2, how many trailing dimensions to calculate with
|
||||
|
||||
Returns
|
||||
-------
|
||||
centroid : torch.Tensor
|
||||
An (i,j) index or stack of indices
|
||||
"""
|
||||
# For some reason this needs to be a list
|
||||
indices = [t.arange(im.shape[-dims+i]).to(t.float32) for i in range(dims)]
|
||||
indices = t.meshgrid(*indices, indexing='ij')
|
||||
|
||||
use_dims = [-dims+i for i in range(dims)]
|
||||
divisor = t.sum(im, dim=use_dims)
|
||||
centroids = [t.sum(index * im, dim=use_dims) / divisor
|
||||
for index in indices]
|
||||
|
||||
return t.stack(centroids,dim=-1)
|
||||
|
||||
|
||||
def centroid_sq(im, dims=2, comp=False):
|
||||
"""Returns the centroid of the square of an image or stack of images
|
||||
|
||||
By default, the last two dimensions are used in the calculation
|
||||
and the remainder of the dimensions are passed through.
|
||||
|
||||
If the "comp" flag is set, it will be assumed that the last dimension
|
||||
represents the real and imaginary part of a complex number, and the
|
||||
centroid will be calculated for the magnitude squared of those numbers
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : torch.Tensor
|
||||
An image or stack of images to calculate from
|
||||
dims : int
|
||||
Default 2, how many trailing dimensions to calculate for
|
||||
comp : bool
|
||||
Default is False, whether the data represents complex numbers
|
||||
|
||||
Returns
|
||||
-------
|
||||
centroid: torch.Tensor
|
||||
An (i,j) index or stack of indices
|
||||
"""
|
||||
if comp:
|
||||
im_sq = t.abs(im)**2
|
||||
else:
|
||||
im_sq = im**2
|
||||
|
||||
return centroid(im_sq, dims=dims)
|
||||
|
||||
|
||||
def sinc_subpixel_shift(im, shift):
|
||||
"""Performs a subpixel shift with sinc interpolation on the given tensor
|
||||
|
||||
The subpixel shift is done circularly via a multiplication with a linear
|
||||
phase mask in Fourier space.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : torch.Tensor
|
||||
A complex-valued tensor to perform the subpixel shift on
|
||||
shift : array
|
||||
A length-2 array describing the shift to perform, in pixels
|
||||
|
||||
Returns
|
||||
-------
|
||||
shifted_im : torch.Tensor
|
||||
The subpixel shifted tensor
|
||||
"""
|
||||
|
||||
i = t.arange(im.shape[0]) - im.shape[0]//2
|
||||
j = t.arange(im.shape[1]) - im.shape[1]//2
|
||||
I,J = t.meshgrid(i,j, indexing='ij')
|
||||
I = 2 * np.pi * I.to(t.float32) / im.shape[0]
|
||||
J = 2 * np.pi * J.to(t.float32) / im.shape[1]
|
||||
I = I.to(dtype=im.dtype,device=im.device)
|
||||
J = J.to(dtype=im.dtype,device=im.device)
|
||||
|
||||
fft_im = t.fft.fftshift(t.fft.fft2(im),dim=(-2,-1))
|
||||
shifted_fft_im = fft_im * t.exp(1j * (-shift[0]*I - shift[1]*J))
|
||||
return t.fft.ifft2(t.fft.ifftshift(shifted_fft_im, dim=(-2,-1)))
|
||||
|
||||
|
||||
|
||||
def find_subpixel_shift(im1, im2, search_around=(0,0), resolution=10):
|
||||
"""Calculates the subpixel shift between two images by maximizing the autocorrelation
|
||||
|
||||
This function only searches in a 2 pixel by 2 pixel box around the
|
||||
specified search_around parameter. The calculation is done using the
|
||||
approach outlined in "Efficient subpixel image registration algorithms",
|
||||
Optics Express (2008) by Manual Guizar-Sicarios et al.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : torch.Tensor
|
||||
The first real or complex-valued torch tensor
|
||||
im2 : torch.Tensor
|
||||
The second real or complex-valued torch tensor
|
||||
search_around : array
|
||||
Default (0,0), the shift to search in the vicinity of
|
||||
resolution : int
|
||||
Default is 10, the fraction of a pixel to calculate to
|
||||
|
||||
Returns
|
||||
-------
|
||||
shift : torch.Tensor
|
||||
The relative shift (i,j) needed to best map im1 onto im2
|
||||
"""
|
||||
|
||||
#
|
||||
# Here's my approach, perhaps it's a little unconventional. I will first
|
||||
# calculate the phase correlation function as found in ____ (cite a paper
|
||||
# defining it). This is strongly peaked, so I can take a small window
|
||||
# of say, 10x10 pixels, and then do a sinc interpolation of that area
|
||||
# using an FFT with upsampling by a factor of resolution in reciprocal
|
||||
# space
|
||||
#
|
||||
cor_fft = t.fft.fft2(im1) * t.conj(t.fft.fft2(im2))
|
||||
|
||||
# Not sure if this is more or less stable than just the correlation
|
||||
# maximum - requires some testing
|
||||
cor = t.fft.ifft2(cor_fft / t.abs(cor_fft))
|
||||
|
||||
|
||||
# Now, I need to shift the array to pull out a contiguous window
|
||||
# around the correlation maximum
|
||||
try:
|
||||
search_around = search_around.cpu()
|
||||
except:
|
||||
search_around = t.as_tensor(search_around)
|
||||
|
||||
window_size = 15
|
||||
shift_zero = tuple(-search_around + t.tensor([window_size,window_size]))
|
||||
cor_window = t.roll(cor, shift_zero, dims=(-2,-1))[...,:2*window_size,:2*window_size]
|
||||
|
||||
# Now we upsample this window
|
||||
cor_window_fft = t.fft.fftshift(t.fft.fft2(cor_window),dim=(-2,-1))
|
||||
upsampled = t.zeros(tuple(t.tensor(cor_window_fft.shape) * resolution),
|
||||
dtype=cor.dtype,device=cor.device)
|
||||
|
||||
upsampled[...,:2*window_size,:2*window_size] = cor_window_fft
|
||||
upsampled = t.roll(upsampled,(-window_size,-window_size),dims=(0,1))
|
||||
upsampled = t.roll(t.abs(t.fft.ifft2(upsampled))**2,
|
||||
(-window_size*resolution,-window_size*resolution),
|
||||
dims=(0,1))
|
||||
|
||||
|
||||
# And we extract the shift from the window
|
||||
sh = t.as_tensor(upsampled.shape, device=upsampled.device)
|
||||
cormax = t.as_tensor([t.div(t.argmax(upsampled), sh[1],
|
||||
rounding_mode='floor'),
|
||||
t.argmax(upsampled) % sh[1]],
|
||||
device=upsampled.device)
|
||||
|
||||
sh_over_2 = t.div(sh,2,rounding_mode='floor')
|
||||
subpixel_shift = ((cormax + sh_over_2) % sh - sh_over_2).to(dtype=upsampled.dtype)
|
||||
|
||||
return search_around.to(device=upsampled.device, dtype=upsampled.dtype) + \
|
||||
subpixel_shift / resolution
|
||||
|
||||
|
||||
def find_pixel_shift(im1, im2):
|
||||
"""Calculates the integer pixel shift between two images by maximizing the autocorrelation
|
||||
|
||||
This function simply takes the circular correlation with an FFT and
|
||||
returns the position of the maximum of that correlation. This corresponds
|
||||
to the amount that im1 would have to be shifted by to line up best with
|
||||
im2
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : torch.Tensor
|
||||
The first real or complex-valued torch tensor
|
||||
im2 : torch.Tensor
|
||||
The second real or complex-valued torch tensor
|
||||
|
||||
Returns
|
||||
-------
|
||||
shift : torch.Tensor
|
||||
The integer-valued shift (i,j) that best maps im1 onto im2
|
||||
"""
|
||||
cor_fft = t.fft.fft2(im1) * t.conj(t.fft.fft2(im2))
|
||||
|
||||
# Not sure if this is more or less stable than just the correlation
|
||||
# maximum - requires some testing
|
||||
cor = t.abs(t.fft.ifft2(cor_fft / t.abs(cor_fft)))
|
||||
|
||||
|
||||
sh = t.as_tensor(cor.shape,device=im1.device)
|
||||
cormax = t.tensor([t.div(t.argmax(cor),sh[1],rounding_mode='floor'),
|
||||
t.argmax(cor) % sh[1]]).to(device=im1.device)
|
||||
|
||||
sh_over_2 = t.div(sh,2,rounding_mode='floor')
|
||||
return (cormax + sh_over_2) % sh - sh_over_2
|
||||
|
||||
|
||||
|
||||
def find_shift(im1, im2, resolution=10):
|
||||
"""Calculates the shift between two images by maximizing the autocorrelation
|
||||
|
||||
This function starts by calculating the maximum shift to integer
|
||||
pixel resolution, and then searchers the nearby area to calculate a
|
||||
subpixel shift
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im1 : torch.Tensor
|
||||
The first real or complex-valued torch tensor
|
||||
im2 : torch.Tensor
|
||||
The second real or complex-valued torch tensor
|
||||
resolution : int
|
||||
Default is 10, the fraction of a pixel to calculate to
|
||||
|
||||
Returns
|
||||
-------
|
||||
shift : torch.Tensor
|
||||
The relative shift (i,j) needed to best map im1 onto im2
|
||||
"""
|
||||
integer_shift = find_pixel_shift(im1,im2)
|
||||
subpixel_shift = find_subpixel_shift(im1, im2, search_around=integer_shift,
|
||||
resolution=resolution)
|
||||
|
||||
return subpixel_shift
|
||||
|
||||
|
||||
def convolve_1d(image, kernel, dim=0, fftshift_kernel=True):
|
||||
"""Convolves an image with a 1d kernel along a specified dimension
|
||||
|
||||
The convolution is a circular convolution calculated using a Fourier
|
||||
transform. The calculation is done so the input remains differentiable
|
||||
with respect to the output.
|
||||
|
||||
If the image has a final dimension of 2, it is assumed to be complex.
|
||||
Otherwise, the image is assumed to be real. The image and kernel
|
||||
must either both be real or both be complex.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : torch.Tensor
|
||||
The image to convolve
|
||||
kernel : torch.Tensor
|
||||
The 1d kernel to convolve with
|
||||
dim : int
|
||||
Default 0, the dimension to convolve along
|
||||
fftshift_kernel : bool
|
||||
Default True, whether to fftshift the kernel first.
|
||||
|
||||
Returns
|
||||
-------
|
||||
convolved_im : torch.Tensor
|
||||
The convolved image
|
||||
"""
|
||||
|
||||
|
||||
if fftshift_kernel:
|
||||
kernel = t.fft.ifftshift(kernel,dim=(-1,))
|
||||
|
||||
# We have to transpose the relevant dimension to -1 before using the fft,
|
||||
# which expects to operate on the final dimension
|
||||
trans_im = t.transpose(image, dim, -1)
|
||||
|
||||
# Take a correlation
|
||||
fft_im = t.fft.fft(trans_im)
|
||||
fft_kernel = t.fft.fft(kernel)
|
||||
trans_conv = t.fft.ifft(fft_im * fft_kernel)
|
||||
|
||||
conv_im = t.transpose(trans_conv, dim, -1)
|
||||
|
||||
return conv_im
|
||||
|
||||
|
||||
def fourier_upsample(ims, preserve_mean=False):
|
||||
# If preserve_mean is true, it preserves the mean pixel intensity
|
||||
# otherwise, it preserves the total summed intensity
|
||||
upsampled = t.zeros(ims.shape[:-2]+(2*ims.shape[-2],2*ims.shape[-1]),
|
||||
dtype=ims.dtype,
|
||||
device=ims.device)
|
||||
left = [ims.shape[-2]//2,ims.shape[-1]//2]
|
||||
right = [ims.shape[-2]//2+ims.shape[-2],
|
||||
ims.shape[-1]//2+ims.shape[-1]]
|
||||
|
||||
upsampled[...,left[0]:right[0],left[1]:right[1]] = propagators.far_field(ims)
|
||||
if preserve_mean:
|
||||
upsampled *= 2
|
||||
return propagators.inverse_far_field(upsampled)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.initializers.initializers import *
|
||||
@@ -0,0 +1,659 @@
|
||||
"""Contains functions to sensibly initialize reconstructions
|
||||
|
||||
The functions in this module both do the geometric calculations needed to
|
||||
initialize the reconstrucions, and the heuristic calculations for
|
||||
geierating sensible initializations for the probe guess.
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
import torch as t
|
||||
from CDTools.tools.propagators import *
|
||||
from CDTools.tools.analysis import orthogonalize_probes
|
||||
from CDTools.tools import image_processing
|
||||
from scipy.fftpack import next_fast_len
|
||||
from scipy.sparse import linalg as spla
|
||||
from torch.nn.functional import pad
|
||||
import numpy as np
|
||||
from functools import *
|
||||
|
||||
__all__ = ['exit_wave_geometry', 'calc_object_setup', 'gaussian',
|
||||
'gaussian_probe', 'SHARP_style_probe', 'STEM_style_probe',
|
||||
'RPI_spectral_init',
|
||||
'generate_subdominant_modes']
|
||||
|
||||
def exit_wave_geometry(det_basis, det_shape, wavelength, distance, center=None, opt_for_fft=True, padding=0, oversampling=1):
|
||||
"""Returns an exit wave basis and shape, as well as a detector slice for the given detector geometry
|
||||
|
||||
It takes in the parameters for a given detector - the basis defining
|
||||
the pixel pitch and the shape, as well as the wavelength and propagation
|
||||
distance. Optionally, it accepts a defined "center", the zero-frequency
|
||||
pixel's location. It then will automatically define a larger detector
|
||||
if necessary, define the exit wave basis associated with a far-field
|
||||
diffraction experiment, and return that basis, shape, and detector slice
|
||||
|
||||
Parameters
|
||||
----------
|
||||
det_basis : torch.Tensor
|
||||
The detector basis, as defined elsewhere
|
||||
det_shape : torch.Size
|
||||
The (i,j) shape of the detector
|
||||
wavelength : float)
|
||||
The wavelength of light for the experiment, in m
|
||||
distance : float
|
||||
The sample-detector distance, in m
|
||||
center : torch.Tensor
|
||||
If defined, the location of the zero frequency pixel
|
||||
opt_for_fft : bool
|
||||
Default is true, whether to increase detector size to improve fft performance
|
||||
padding : int
|
||||
Default is 0, the size of an extra border of nonphysical pixels around the detector
|
||||
oversampling : int
|
||||
Default is 1, the amount to multiply the exit wave shape by.
|
||||
|
||||
Returns
|
||||
-------
|
||||
basis : torch.Tensor
|
||||
The exit wave basis
|
||||
shape : torch.Tensor
|
||||
The exit wave's shape
|
||||
slice : slice
|
||||
The slice corresponding to the physical detector
|
||||
"""
|
||||
|
||||
det_shape = t.as_tensor(tuple(det_shape), dtype=t.int32)
|
||||
det_basis = t.as_tensor(det_basis)
|
||||
# First, set the center if it's not already specified
|
||||
# This definition matches the center pixel of an fftshifted array
|
||||
if center is None:
|
||||
# this is center//2, but in pytorch 1.9.0 it throws a warning
|
||||
# if you just do that
|
||||
|
||||
center = t.div(det_shape,2,rounding_mode='floor')# // 2
|
||||
else:
|
||||
center = t.as_tensor(center, dtype=t.int32)
|
||||
|
||||
# Then, calculate the required detector size from the centering
|
||||
# This is a bit opaque but was worth doing accurately
|
||||
min_left = center * 2
|
||||
min_right = (det_shape - center) * 2 - 1
|
||||
full_shape = t.max(min_left,min_right).to(t.int32) + 2 * padding
|
||||
|
||||
# In some edge cases this shape can be smaller than the detector shape
|
||||
full_shape = t.max(full_shape, det_shape)
|
||||
|
||||
|
||||
if opt_for_fft:
|
||||
full_shape = t.as_tensor([next_fast_len(dim) for dim in full_shape],
|
||||
dtype=t.int32)
|
||||
|
||||
# Then, generate a slice that pops the actual detector from the full
|
||||
# detector shape
|
||||
# this is full_center//2, but in pytorch 1.9.0 it throws a warning
|
||||
# if you just do that
|
||||
full_center = t.div(full_shape,2, rounding_mode='floor')# // 2
|
||||
det_slice = np.s_[int(full_center[0]-center[0]):
|
||||
int(full_center[0]-center[0]+det_shape[0]),
|
||||
int(full_center[1]-center[1]):
|
||||
int(full_center[1]-center[1]+det_shape[1])]
|
||||
|
||||
|
||||
# Finally, generate the basis for the exit wave in real space
|
||||
|
||||
# This method should work for a general parallelogram
|
||||
# shaped detector
|
||||
det_shape = det_basis * full_shape.to(t.float32)
|
||||
pinv_basis = t.tensor(np.linalg.pinv(det_shape).transpose()).to(t.float32)
|
||||
real_space_basis = pinv_basis * wavelength * distance
|
||||
|
||||
# This is definitely correct, but less simple. Included here
|
||||
# So future me can check that both versions are consistent.
|
||||
#oop_dir = np.cross(det_basis[:,0],det_basis[:,1])
|
||||
#oop_dir /= np.linalg.norm(oop_dir)
|
||||
#full_basis = np.array([np.array(det_basis[:,0]),np.array(det_basis[:,1]),oop_dir]).transpose()
|
||||
#inv_basis = t.tensor(np.linalg.inv(full_basis)[:2,:].transpose()).to(t.float32)
|
||||
#real_space_basis = inv_basis*wavelength * distance / \
|
||||
# full_shape.to(t.float32)
|
||||
|
||||
# Finally, convert the shape back to a torch.Size
|
||||
full_shape = t.Size([dim * oversampling for dim in full_shape])
|
||||
|
||||
|
||||
return real_space_basis, full_shape, det_slice
|
||||
|
||||
|
||||
def calc_object_setup(probe_shape, translations, padding=0):
|
||||
"""Returns an object shape and minimum pixel translation
|
||||
|
||||
Based on the given pixel-space translations, it will calculate the
|
||||
required size for an object array and calculate the pixel translation
|
||||
that corresponds to a shift by (0,0) of the probe.
|
||||
|
||||
Optionally a small extra border can be defined via the padding
|
||||
attribute. If this is done, the calculated pixel translation will
|
||||
correspond to (padding,padding)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe_shape : torch.Size
|
||||
The size of the probe array
|
||||
translations : torch.Tensor
|
||||
A Jx2 stack of pixel-valued (i,j) translations
|
||||
padding : int
|
||||
Optional, the size of an extra border to include
|
||||
|
||||
Returns
|
||||
-------
|
||||
obj_shape : torch.Size
|
||||
The minimum required size of the object array
|
||||
min_translation : torch.Tensor
|
||||
The minimum pixel-valued translation
|
||||
"""
|
||||
|
||||
# First we look at the translations to find the minimum translation
|
||||
# and the range of translations
|
||||
min_translation = t.min(translations, dim=0)[0]
|
||||
translation_range = t.max(translations, dim=0)[0] - min_translation
|
||||
|
||||
# Calculate the required shape
|
||||
translation_range = t.ceil(translation_range).numpy().astype(np.int32)
|
||||
shape = translation_range + np.array(probe_shape) + 2 * padding
|
||||
shape = t.Size(shape)
|
||||
|
||||
# And the minimum translation
|
||||
min_translation = min_translation - padding
|
||||
|
||||
return shape, min_translation
|
||||
|
||||
|
||||
|
||||
def gaussian(shape, sigma, amplitude=1, center = None, curvature=[0,0]):
|
||||
"""Returns an array with a centered Gaussian
|
||||
|
||||
Takes in the shape and standard deviation of a gaussian
|
||||
and returns a complex torch tensor (trailing dimension is 2) with
|
||||
values corresponding to a two-dimensional gaussian function
|
||||
|
||||
Note that [0, 0] is taken to be at the upper left corner of the array.
|
||||
Default is centered at ((shape[0]-1)/2, (shape[1]-1)/2)) because x and y are zero-indexed.
|
||||
By default, the phase is uniformly 0, however a curvature can be
|
||||
specified to simulate a probe that has been propagated a known distance
|
||||
from it's focal point. The curvature is implemented by adding a quadratic
|
||||
phase phi = exp(i*curvature/2 r^2) to the Gaussian
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : array
|
||||
A 1x2 array specifying the dimensions of the output array in the form (i shape, j shape)
|
||||
sigma : array
|
||||
A 1x2 array specifying the i- and j- standard deviation of the gaussian in the form (i stdev, j stdev)
|
||||
amplitude : float
|
||||
Default 1, the amplitude the gaussian to simulate
|
||||
center : array
|
||||
Optional, a 1x2 array specifying the location of the center of the gaussian (i center, j center)
|
||||
curvature : array
|
||||
Optional, a complex part to add to the gaussian coefficient
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
The complex-style tensor storing the Gaussian
|
||||
"""
|
||||
if center is None:
|
||||
center = ((shape[0]-1)/2, (shape[1]-1)/2)
|
||||
|
||||
i, j = np.mgrid[:shape[0], :shape[1]]
|
||||
isq = (i - center[0])**2
|
||||
jsq = (j - center[1])**2
|
||||
result = np.exp((1j*curvature[0] / 2 - 1 / (2 * sigma[0]**2)) * isq + \
|
||||
(1j*curvature[1] / 2 - 1 / (2 * sigma[1]**2)) * jsq)
|
||||
return t.as_tensor(amplitude*result,dtype=t.complex64)
|
||||
|
||||
|
||||
|
||||
def gaussian_probe(dataset, basis, shape, sigma, propagation_distance=0, polarized=False, left_polarized=True):
|
||||
"""Initializes a gaussian probe based on experimental parameters
|
||||
|
||||
This function generates a gaussian probe initialization which has a
|
||||
total fluence matching the order of magnitude of the intensity in
|
||||
the observed dataset, provided the object function is of order 1.
|
||||
|
||||
The initialization is done using parameters defined in physical units,
|
||||
such as sigma (in meters) and the propagation distance (in meters).
|
||||
The internal conversion to pixel space is done with a provided probe
|
||||
basis and probe shape.
|
||||
|
||||
TODO: Should be updated to accept a mask
|
||||
|
||||
Sigma can be provided either as a scalar for a uniform beam, or as
|
||||
an iterable of length 2 with [sigma_i, sigma_j] being the components
|
||||
of sigma in the directions parallel to the i and j basis vectors of
|
||||
the probe basis
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : Ptycho_2D_Dataset
|
||||
The dataset whose intensity we want to match
|
||||
basis : array
|
||||
The real space basis for exit waves in our experiment
|
||||
shape : array
|
||||
The shape of the simulated real space arrays
|
||||
sigma : array
|
||||
The standard deviation of the probe at it's focus
|
||||
propagation_distance : float
|
||||
Default 0, a distance to propagate the gaussian from it's focus
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
The complex-style tensor storing the Gaussian
|
||||
"""
|
||||
# First, we want to generate the parameters (sigma and curvature) for the
|
||||
# propagated gaussian. Ignore the purely z-dependent phases
|
||||
wavelength = dataset.wavelength
|
||||
if propagation_distance is None:
|
||||
propagation_distance = 0
|
||||
|
||||
z = propagation_distance # for shorthand
|
||||
|
||||
sigma = np.array(sigma)
|
||||
k = 2 * np.pi / wavelength
|
||||
zr = k * sigma**2
|
||||
sigmaz = sigma * np.sqrt(1 + (z / zr)**2)
|
||||
curvature = -k * z / (z**2 + zr**2)
|
||||
|
||||
# The conversion must then be done to pixel space
|
||||
sigma_pix = sigmaz / np.array([np.linalg.norm(basis[:,0]),
|
||||
np.linalg.norm(basis[:,1])])
|
||||
curvature_pix = curvature * np.array([np.linalg.norm(basis[:,0]),
|
||||
np.linalg.norm(basis[:,1])])**2
|
||||
|
||||
# Then we can generate the gaussian array
|
||||
probe = gaussian(shape, sigma=sigma_pix, curvature=curvature_pix)
|
||||
|
||||
# Finally, we should calculate the average pattern intensity from the
|
||||
# dataset and normalize the gaussian probe. This should be done by
|
||||
if not polarized:
|
||||
avg_intensities = [t.sum(dataset[idx][1]) for idx in range(len(dataset))]
|
||||
|
||||
else:
|
||||
polarizer = dataset.polarizer.tolist()
|
||||
analyzer = dataset.analyzer.tolist()
|
||||
factors = [(math.cos(math.radians(polarizer[idx] - analyzer[idx])))**2 for idx in range(len(dataset)) if (abs(polarizer[idx] - analyzer[idx]) > 5)]
|
||||
avg_intensities = [t.sum(dataset[idx][1]) / factor[idx] for idx in range(len(dataset))]
|
||||
|
||||
avg_intensity = t.mean(t.tensor(avg_intensities))
|
||||
probe_intensity = t.sum(t.abs(probe)**2)
|
||||
probe = t.sqrt(avg_intensity / probe_intensity) * probe
|
||||
|
||||
if polarized:
|
||||
if left_polarized == True:
|
||||
x = 1j
|
||||
else:
|
||||
x = -1j
|
||||
probe = t.stack((probe.to(dtype=t.cfloat), x * probe.to(dtype=t.cfloat)), dim=-4)
|
||||
return probe[..., None, :, :]
|
||||
|
||||
return probe
|
||||
|
||||
|
||||
|
||||
|
||||
def SHARP_style_probe(dataset, shape, det_slice, propagation_distance=None, oversampling=1, polarized=False, left_polarized=True):
|
||||
"""Generates a SHARP style probe guess from a dataset
|
||||
|
||||
What we call the "SHARP" style probe guess is to take a mean of all
|
||||
the diffraction patterns and use that as an initial guess of the
|
||||
Fourier space distribution of the probe. We set all the phases to
|
||||
zero, which would for many simple beams (like a zone plate) generate
|
||||
a first guess of the probe that is very close to the focal spot of
|
||||
the probe beam.
|
||||
|
||||
If the probe is simulated in higher resolution than the detector,
|
||||
a common occurence, these undefined pixels are set to zero for the
|
||||
purposes of defining the guess
|
||||
|
||||
We make a small tweak to this procedure to lower the central pixel of
|
||||
the probe generated this way, which can often overwhelm the rest of the
|
||||
probe if there is significant noise on the detector
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : Ptycho_2D_Dataset
|
||||
The dataset to work from
|
||||
shape : torch.Size
|
||||
The size of the probe array to simulate
|
||||
det_slice : slice
|
||||
A slice or tuple of slices corresponding to the detector region in Fourier space
|
||||
propagation_distance : float
|
||||
Default is no propagation, an amount to propagate the guessed probe from it's focal point
|
||||
oversampling : int
|
||||
Default 1, the width of the region of pixels in the wavefield to bin into a single detector pixel
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
The complex-style tensor storing the probe guess
|
||||
"""
|
||||
|
||||
# NOTE: I don't love the way np and torch are mixed here, I think this
|
||||
# function deserves some love.
|
||||
|
||||
# to use the mask or not?
|
||||
intensities = np.zeros([dim // oversampling for dim in shape])
|
||||
|
||||
if polarized:
|
||||
factors = [(math.cos(math.radians(polarizer[idx] - analyzer[idx])))**2 for idx in range(len(dataset)) if abs(polarizer[idx] - analyzer[idx]) > 5]
|
||||
else:
|
||||
factors = [1 for idx in range(len(dataset))]
|
||||
|
||||
|
||||
for params, im in dataset:
|
||||
if hasattr(dataset,'mask') and dataset.mask is not None:
|
||||
intensities[det_slice] += dataset.mask.cpu().numpy() * im.cpu().numpy() / factors[params[0]]
|
||||
else:
|
||||
intensities[det_slice] += im.cpu().numpy() / params[factors[0]]
|
||||
intensities /= len(dataset)
|
||||
|
||||
|
||||
|
||||
# Subtract off a known background if it's stored
|
||||
if hasattr(dataset, 'background') and dataset.background is not None:
|
||||
intensities[det_slice] = np.clip(intensities[det_slice] - dataset.background.cpu().numpy(), a_min=0,a_max=None)
|
||||
|
||||
probe_fft = t.tensor(np.sqrt(intensities)).to(dtype=t.complex64)
|
||||
|
||||
probe_guess = inverse_far_field(probe_fft).numpy()
|
||||
# Now we remove the central pixel
|
||||
#center = np.array(probe_guess.shape) // 2
|
||||
|
||||
# I'm always unsure whether to use this modification:
|
||||
|
||||
#probe_guess[center[0], center[1]]=np.mean([
|
||||
# probe_guess[center[0]-1, center[1]],
|
||||
# probe_guess[center[0]+1, center[1]],
|
||||
# probe_guess[center[0], center[1]-1],
|
||||
# probe_guess[center[0], center[1]+1]])
|
||||
|
||||
probe_guess = t.as_tensor(probe_guess, dtype=t.complex64)
|
||||
|
||||
if propagation_distance is not None:
|
||||
# First generate the propagation array
|
||||
|
||||
probe_shape = t.as_tensor(tuple(probe_guess.shape))
|
||||
|
||||
# Start by recalculating the probe basis from the given information
|
||||
det_basis = t.as_tensor(dataset.detector_geometry['basis'])
|
||||
basis_dirs = det_basis / t.norm(det_basis, dim=0)
|
||||
distance = dataset.detector_geometry['distance']
|
||||
probe_basis = basis_dirs * dataset.wavelength * distance / \
|
||||
(probe_shape * t.norm(det_basis,dim=0))
|
||||
|
||||
# Then package everything as it's needed
|
||||
probe_spacing = t.norm(probe_basis,dim=0).numpy()
|
||||
probe_shape = probe_shape.numpy().astype(np.int32)
|
||||
|
||||
# And generate the propagator
|
||||
AS_prop = generate_angular_spectrum_propagator(probe_shape, probe_spacing, dataset.wavelength, propagation_distance)
|
||||
|
||||
probe_guess = near_field(probe_guess,AS_prop)
|
||||
|
||||
# Finally, place this probe in a full-sized array if there is oversampling
|
||||
final_probe = t.zeros(shape,dtype=t.complex64)
|
||||
left = shape[0]//2 - probe_guess.shape[0] // 2
|
||||
top = shape[1]//2 - probe_guess.shape[1] // 2
|
||||
final_probe[left:left+probe_guess.shape[0],
|
||||
top:top+probe_guess.shape[1]] = probe_guess
|
||||
|
||||
if polarized:
|
||||
if left_polarized:
|
||||
x = 1j
|
||||
else:
|
||||
x = -1j
|
||||
final_probe = t.stack((final_probe.to(dtype=t.cfloat), final_probe.to(dtype=t.cfloat)), dim=-4)
|
||||
final_probe = final_probe[..., None, :, :]
|
||||
|
||||
return final_probe
|
||||
|
||||
|
||||
def STEM_style_probe(dataset, shape, det_slice, convergence_semiangle, propagation_distance=None, oversampling=1):
|
||||
"""Generates a STEM style probe guess from a dataset
|
||||
|
||||
What we call the "STEM" style probe guess is a probe generated by
|
||||
a uniform aperture in Fourier space, with no optical aberrations.
|
||||
This is the kind of probe than an ideal, aberration-corrected STEM
|
||||
with perfect coherence would produce, so it is a good starting
|
||||
guess for STEM datasets.
|
||||
|
||||
We set the initial intensity of the probe with relation to the
|
||||
diffraction patterns so that a typical object will have an intensity
|
||||
of around 1. We also set the initial phase ramp of the probe such that
|
||||
the undiffracted probe has a centroid on the detector which matches the
|
||||
centroid of the diffraction patterns
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dataset : Ptycho_2D_Dataset
|
||||
The dataset to work from
|
||||
shape : torch.Size
|
||||
The size of the probe array to simulate
|
||||
det_slice : slice
|
||||
A slice or tuple of slices corresponding to the detector region in Fourier space
|
||||
convergence_angle : float
|
||||
The convergence angle of the probe, in mrad.
|
||||
propagation_distance : float
|
||||
Default is no propagation, an amount to propagate the guessed probe from it's focal point
|
||||
oversampling : int
|
||||
Default 1, the width of the region of pixels in the wavefield to bin into a single detector pixel
|
||||
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
The complex-style tensor storing the probe guess
|
||||
"""
|
||||
|
||||
# The basis in the dataset should describe the basis of the probe's
|
||||
# Fourier transform, even if the probe is simulated on larger stage than
|
||||
# the detector. The only issue is if the probe is oversampled in
|
||||
# Fourier space (simulated on a larger stage in real space). That factor
|
||||
# is defined by oversampling.
|
||||
|
||||
probe_basis = (t.as_tensor(dataset.detector_geometry['basis'],
|
||||
dtype=t.float32)
|
||||
/ oversampling)
|
||||
|
||||
mean_im = t.mean(dataset.patterns,dim=0)
|
||||
center = image_processing.centroid(mean_im)
|
||||
|
||||
Is = t.arange(shape[0], dtype=t.float32) - center[0]
|
||||
Js = t.arange(shape[1], dtype=t.float32) - center[1]
|
||||
Is,Js = t.meshgrid(Is,Js)
|
||||
|
||||
Rs = t.tensordot(probe_basis,t.stack([Is,Js]),dims=1)
|
||||
forward = t.Tensor([0,0,1])
|
||||
Rs = (forward * dataset.detector_geometry['distance'])[:,None,None] + Rs
|
||||
dirs = Rs / t.linalg.norm(Rs,dim=(0,))
|
||||
angles = t.acos(t.tensordot(forward,dirs,dims=1)) * 1000 #in mrad
|
||||
|
||||
probe_fft = t.zeros(shape,dtype=t.complex64)
|
||||
probe_fft[angles<convergence_semiangle] = 1
|
||||
|
||||
probe_mean = t.mean(t.abs(probe_fft)**2)
|
||||
diff_mean = t.mean(mean_im)
|
||||
probe_fft = probe_fft * t.sqrt(diff_mean / probe_mean)
|
||||
|
||||
probe_guess = inverse_far_field(probe_fft)
|
||||
|
||||
if propagation_distance is not None:
|
||||
# It's probably worth checking if this is correct when oversampling
|
||||
# is not 1
|
||||
|
||||
probe_shape = t.as_tensor(tuple(probe_guess.shape))
|
||||
|
||||
# Start by recalculating the probe basis from the given information
|
||||
det_basis = t.as_tensor(dataset.detector_geometry['basis'])
|
||||
basis_dirs = det_basis / t.norm(det_basis, dim=0)
|
||||
distance = dataset.detector_geometry['distance']
|
||||
probe_basis = basis_dirs * dataset.wavelength * distance / \
|
||||
(probe_shape * t.norm(det_basis,dim=0))
|
||||
|
||||
# Then package everything as it's needed
|
||||
probe_spacing = t.norm(probe_basis,dim=0).numpy()
|
||||
probe_shape = probe_shape.numpy().astype(np.int32)
|
||||
|
||||
# And generate the propagator
|
||||
AS_prop = generate_angular_spectrum_propagator(probe_shape, probe_spacing, dataset.wavelength, propagation_distance)
|
||||
|
||||
probe_guess = near_field(probe_guess,AS_prop)
|
||||
|
||||
# Finally, place this probe in a full-sized array if there is oversampling
|
||||
final_probe = t.zeros(shape,dtype=t.complex64)
|
||||
left = shape[0]//2 - probe_guess.shape[0] // 2
|
||||
top = shape[1]//2 - probe_guess.shape[1] // 2
|
||||
final_probe[left:left+probe_guess.shape[0],
|
||||
top:top+probe_guess.shape[1]] = probe_guess
|
||||
|
||||
return final_probe
|
||||
|
||||
|
||||
def RPI_spectral_init(pattern, probe, obj_shape, n_modes=1, mask=None, background=None):
|
||||
|
||||
# First, check if the probe is a single mode or many modes.
|
||||
# If the probe is many modes, orthogonalize it and use the top mode
|
||||
# for initialization
|
||||
if probe.dim() == 4:
|
||||
probe = orthogonalize_probes(probe)[0]
|
||||
|
||||
pad0l = (probe.shape[-2] - obj_shape[0])//2
|
||||
pad0r = probe.shape[-2] - obj_shape[0] - pad0l
|
||||
pad1l = (probe.shape[-1] - obj_shape[1])//2
|
||||
pad1r = probe.shape[-1] - obj_shape[1] - pad1l
|
||||
|
||||
def a_dagger(im):
|
||||
im = t.tensor(im.reshape(obj_shape)).to(dtype=t.complex64)
|
||||
im = inverse_far_field(pad(far_field(im), (pad1l,pad1r,pad0l,pad0r)))
|
||||
exit_wave = probe * im
|
||||
return far_field(exit_wave).numpy().ravel()
|
||||
|
||||
def a(measured):
|
||||
measured = t.tensor(measured.reshape(pattern.shape[0],pattern.shape[1])).to(dtype=t.complex64)
|
||||
im = inverse_far_field(measured)
|
||||
multiplied = t.conj(probe) * im
|
||||
backplane = far_field(multiplied)
|
||||
clipped = backplane[pad0l:pad0l+obj_shape[0],
|
||||
pad1l:pad1l+obj_shape[1]]
|
||||
return (inverse_far_field(clipped)).numpy().ravel()
|
||||
|
||||
patsize = pattern.shape[0]*pattern.shape[1]
|
||||
imsize = obj_shape[0]*obj_shape[1]
|
||||
probesize = probe.shape[0]*probe.shape[1]
|
||||
A_dagger = spla.LinearOperator((patsize, imsize),matvec=a_dagger)
|
||||
A = spla.LinearOperator((imsize,patsize),matvec=a)
|
||||
|
||||
# Correct the pattern for the background and mask
|
||||
np_pattern = pattern.numpy()
|
||||
if background is not None:
|
||||
np_pattern = np_pattern - background.numpy()
|
||||
if mask is not None:
|
||||
np_pattern = np_pattern * mask.numpy()
|
||||
np_pattern = np.abs(np_pattern.ravel())
|
||||
|
||||
realspace_intensities = A * A_dagger * np.ones(imsize)
|
||||
vec = A_dagger * realspace_intensities
|
||||
def y(measured):
|
||||
#return measured * np_pattern
|
||||
# This normalizes the intensities to account for the fact
|
||||
# that some pixels draw from way more spots on the detector than
|
||||
# others
|
||||
return measured * np_pattern / np.abs(vec)
|
||||
|
||||
Y = spla.LinearOperator((patsize, patsize),matvec=y)
|
||||
eigval, z0 = spla.eigs(A * Y * A_dagger, k=n_modes, which='LM')
|
||||
z0 = z0.transpose().reshape(n_modes,obj_shape[0], obj_shape[1])
|
||||
|
||||
# Now we set the overall scale and relative weights of the guess
|
||||
scale_factor = np.sqrt(np.sum(np_pattern) /
|
||||
t.sum(t.abs(probe)**2).numpy())
|
||||
relative_weights = eigval / np.sum(eigval**2)
|
||||
|
||||
z0 = z0 * (scale_factor * relative_weights[:,None,None])
|
||||
# Now we have to normalize the modes by their eigenvalues
|
||||
|
||||
return t.as_tensor(z0, dtype=t.complex64)
|
||||
|
||||
|
||||
def generate_subdominant_modes(dominant_mode, n_modes, circular=True):
|
||||
"""Generates guesses of subdominant modes based on spatial derivatives
|
||||
|
||||
The idea here is that vibration is an extremely common cause of
|
||||
spatial incoherence. This typically presents as subdominant modes that
|
||||
look like the derivative of the dominant mode along various axes.
|
||||
|
||||
Therefore, it is a good starting guess if the guess of the dominant
|
||||
mode is reasonable. The output probes will all be normalized to have
|
||||
the same intensity as the input dominant probe
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dominant_mode : array
|
||||
The complex-valued dominant mode to work from
|
||||
n_modes : int
|
||||
The number of additional modes to create
|
||||
circular : bool
|
||||
Default True, whether to use circular modes (x+iy) or linear (x,y)
|
||||
Returns
|
||||
-------
|
||||
torch.Tensor
|
||||
The complex-style tensor storing the probe guesses
|
||||
"""
|
||||
|
||||
# This generates a list of tuples, where each tuple (a,b) corresponds
|
||||
# a term (kx^a)*(ky^b), or (kx+iky)^a*(kx-iky)^b for circular modes
|
||||
def make_orders(n_orders):
|
||||
if n_orders >1024:
|
||||
raise KeyError('Are you sure you want this many orders?')
|
||||
i = 1
|
||||
n = 0
|
||||
total = 0
|
||||
while True:
|
||||
if total >= n_orders:
|
||||
break
|
||||
|
||||
yield (n,i-n)
|
||||
total += 1
|
||||
if n<i:
|
||||
n+=1
|
||||
else:
|
||||
n = 0
|
||||
i += 1
|
||||
|
||||
# Gotta check and convert to pytorch if it's numpy
|
||||
|
||||
# First step is to take the FFT of the probe
|
||||
dominant_fft = far_field(dominant_mode)
|
||||
|
||||
shape = dominant_mode.shape
|
||||
center = ((shape[-2])//2, (shape[-1])//2)
|
||||
|
||||
i, j = np.mgrid[:shape[-2], :shape[-1]]
|
||||
i = t.tensor(i - center[0]).to(dtype=dominant_fft.dtype,
|
||||
device=dominant_fft.device)
|
||||
j = t.tensor(j - center[1]).to(dtype=dominant_fft.dtype,
|
||||
device=dominant_fft.device)
|
||||
|
||||
if circular:
|
||||
a = i + 1j * j
|
||||
b = i - 1j * j
|
||||
else:
|
||||
a = i
|
||||
b = j
|
||||
|
||||
probe_norm = t.sum(t.abs(dominant_mode)**2)
|
||||
# Then we need to multiply that FFT by various phase masks and IFFT
|
||||
probes = []
|
||||
for a_order,b_order in make_orders(n_modes):
|
||||
mask = reduce(t.mul,[a]*a_order+[b]*b_order)
|
||||
new_probe = inverse_far_field(mask * dominant_fft)
|
||||
probes.append(new_probe * (probe_norm / t.sum(t.abs(new_probe))))
|
||||
|
||||
return t.stack(probes)
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.interactions.interactions import *
|
||||
@@ -0,0 +1,633 @@
|
||||
""" This module contains various to simulate stages in the probe-sample interaction
|
||||
|
||||
All the tools here are designed to work with automatic differentiation. Each
|
||||
function simulates some aspect of an interaction model that can be used
|
||||
for ptychographic reconstruction.
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
import numpy as np
|
||||
from CDTools.tools import propagators, image_processing, polarization
|
||||
|
||||
__all__ = ['translations_to_pixel', 'pixel_to_translations',
|
||||
'project_translations_to_sample',
|
||||
'ptycho_2D_round','ptycho_2D_linear','ptycho_2D_sinc',
|
||||
'RPI_interaction']
|
||||
|
||||
|
||||
def translations_to_pixel(basis, translations, surface_normal=t.Tensor([0.,0.,1.])):
|
||||
"""Takes real space translations and outputs them in pixel space
|
||||
|
||||
This works for any 2D ptychography geometry. It takes in
|
||||
A set of translations in (x,y) space and outputs the same translations
|
||||
in internal pixel units perpendicular to the detector.
|
||||
|
||||
It uses information on the wavefield basis and, if defined, the
|
||||
sample normal, to perform the conversion.
|
||||
|
||||
The assumed geometry is incoming radiation with a wavevector parallel
|
||||
to the +z axis, [0,0,1]. The default sample orientation has a surface
|
||||
normal parallel to this direction
|
||||
|
||||
Parameters
|
||||
----------
|
||||
basis : torch.Tensor
|
||||
The real space basis the wavefields are defined in
|
||||
translations : torch.Tensor
|
||||
A Jx3 stack of real-space translations, or a single translation
|
||||
surface_normal : torch.Tensor
|
||||
Optional, the sample's surface normal
|
||||
|
||||
Returns
|
||||
-------
|
||||
pixel_translations : torch.Tensor
|
||||
A Jx2 stack of translations in internal (i,j) pixel-space, or a single translation
|
||||
"""
|
||||
|
||||
projection_1 = t.Tensor([[1,0,0],
|
||||
[0,1,0],
|
||||
[0,0,0]]).to(device=translations.device,dtype=translations.dtype)
|
||||
projection_2 = t.inverse(t.Tensor([[1,0,0],
|
||||
[0,1,0],
|
||||
-surface_normal/
|
||||
surface_normal[2]])).to(device=translations.device,dtype=translations.dtype)
|
||||
basis_vectors_inv = t.pinverse(basis).to(device=translations.device,
|
||||
dtype=translations.dtype)
|
||||
projection = t.mm(basis_vectors_inv,
|
||||
t.mm(projection_2,projection_1))
|
||||
projection = projection.t()
|
||||
|
||||
single_translation = False
|
||||
if len(translations.shape) == 1:
|
||||
translations = translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
pixel_translations = t.mm(translations, projection)
|
||||
|
||||
if single_translation:
|
||||
return pixel_translations[0]
|
||||
else:
|
||||
return pixel_translations
|
||||
|
||||
|
||||
def pixel_to_translations(basis, pixel_translations, surface_normal=t.Tensor([0,0,1])):
|
||||
"""Takes pixel-space translations and outputs them in real space
|
||||
|
||||
This works for any 2D ptychography geometry. It takes in
|
||||
A set of internal pixel unit translations in (i,j) space and
|
||||
outputs the same translations real (x,y) space
|
||||
|
||||
It uses information on the wavefield basis and, if defined, the
|
||||
sample normal, to perform the conversion.
|
||||
|
||||
The assumed geometry is incoming radiation with a wavevector parallel
|
||||
to the +z axis, [0,0,1]. The default sample orientation has a surface
|
||||
normal parallel to this direction. Because of this, the z direction
|
||||
translation is always set to zero in the conversion
|
||||
|
||||
Parameters
|
||||
----------
|
||||
basis : torch.Tensor
|
||||
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
|
||||
|
||||
Returns
|
||||
-------
|
||||
real_translations : torch.Tensor
|
||||
A Jx3 stack of real-space translations, or a single translation
|
||||
"""
|
||||
projection_1 = t.Tensor([[1,0,0],
|
||||
[0,1,0],
|
||||
[0,0,0]]).to(device=basis.device,dtype=basis.dtype)
|
||||
projection_2 = t.inverse(t.Tensor([[1,0,0],
|
||||
[0,1,0],
|
||||
-surface_normal/
|
||||
surface_normal[2]])).to(device=basis.device,dtype=basis.dtype)
|
||||
basis_vectors_inv = t.pinverse(basis)
|
||||
projection = t.mm(basis_vectors_inv,
|
||||
t.mm(projection_2,projection_1))
|
||||
# Literally just need the pseudoinverse of the projection we used to go
|
||||
# the other way
|
||||
projection = t.pinverse(projection).t()
|
||||
|
||||
single_translation = False
|
||||
if len(pixel_translations.shape) == 1:
|
||||
pixel_translations = pixel_translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
translations = t.mm(pixel_translations, projection)
|
||||
|
||||
if single_translation:
|
||||
return translations[0]
|
||||
else:
|
||||
return translations
|
||||
|
||||
|
||||
|
||||
def project_translations_to_sample(sample_basis, translations):
|
||||
"""Takes real space translations and outputs them in pixels in a sample basis
|
||||
|
||||
This projection function is designed for the Bragg2DPtycho class. More
|
||||
broadly, it works to take a set of translations in the lab frame and
|
||||
convert each one into two values. First, an (i,j) value in pixels
|
||||
describing the location of the probe's intersection with the sample
|
||||
plane, assuming the basis found in sample_basis is used. Second, an
|
||||
amount that the probe must be propagated along the z-axis to reach
|
||||
the sample plane at the given location. This includes both the effect of
|
||||
the tilted sample plane and any explicitly defined motion along the z axis
|
||||
of the probe-forming optic as included in the input translations.
|
||||
|
||||
|
||||
Note that because of the sign convention (that this function returns th
|
||||
relative amount the probe needs to be propagated to reach any given
|
||||
location), a positive motion along the z-axis of the probe forming optics
|
||||
will lead to a negative propagation distance.
|
||||
|
||||
The assumed geometry is incoming radiation with a wavevector parallel
|
||||
to the +z axis, [0,0,1].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample_basis : torch.Tensor
|
||||
The real space basis the wavefields are defined in
|
||||
translations : torch.Tensor
|
||||
A Jx3 stack of real-space translations, or a single translation
|
||||
|
||||
Returns
|
||||
-------
|
||||
pixel_translations : torch.Tensor
|
||||
A Jx2 stack of translations in internal (i,j) pixel-space, or a single translation
|
||||
propagations : torch.Tensor
|
||||
A length-J vector of propagation distances, in meters
|
||||
"""
|
||||
|
||||
# Must do this all in pytorch unfortunately
|
||||
# First we calculate the surface normal for the projection onto the sample
|
||||
surface_normal = t.cross(sample_basis[:,1],sample_basis[:,0])
|
||||
surface_normal /= t.norm(surface_normal)
|
||||
|
||||
|
||||
# Then we calculate a matrix which can do the projection
|
||||
|
||||
propagation_dir = t.Tensor(np.array([0,0,1])).to(
|
||||
device=surface_normal.device,
|
||||
dtype=surface_normal.dtype)
|
||||
|
||||
I = t.eye(3).to(
|
||||
device=surface_normal.device,
|
||||
dtype=surface_normal.dtype)
|
||||
|
||||
# Here we're setting up a matrix-vector equation mat*answer=input
|
||||
# At some point ger will need to be replaced by outer, but for now
|
||||
# outer many places still don't have new enough versions of torch.
|
||||
mat = t.cat((I - t.ger(propagation_dir,propagation_dir),
|
||||
surface_normal.unsqueeze(0)))
|
||||
|
||||
# And we invert the matrix to do the projection
|
||||
projector = t.pinverse(mat)[:,:3].to(device=translations.device,
|
||||
dtype=translations.dtype)
|
||||
|
||||
# Finally, we need to get the result in the right basis, so this will
|
||||
# do that conversion for us
|
||||
basis_vectors_inv = t.pinverse(sample_basis).to(device=translations.device,
|
||||
dtype=translations.dtype)
|
||||
|
||||
# Same for the propagation direction to extract how far it must propagate
|
||||
propagation_dir_inv = propagation_dir.unsqueeze(0).to(
|
||||
device=translations.device,
|
||||
dtype=translations.dtype)
|
||||
|
||||
|
||||
sample_projection = t.mm(basis_vectors_inv, projector).t()
|
||||
prop_projection = t.mm(propagation_dir_inv, projector).t()
|
||||
|
||||
|
||||
single_translation = False
|
||||
if len(translations.shape) == 1:
|
||||
translations = translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
pixel_translations = t.mm(translations, sample_projection)
|
||||
propagations = t.mm(translations, prop_projection) \
|
||||
- t.mm(translations,propagation_dir[:,None])
|
||||
|
||||
if single_translation:
|
||||
return pixel_translations[0], propagations[0]
|
||||
else:
|
||||
return pixel_translations, propagations
|
||||
|
||||
|
||||
|
||||
|
||||
def ptycho_2D_round(probe, obj, translations, multiple_modes=False, upsample_obj=False):
|
||||
"""Returns a stack of exit waves without accounting for subpixel shifts
|
||||
|
||||
This function returns a collection of exit waves, with the first
|
||||
dimension as the translation index and the final dimensions
|
||||
corresponding to the detector. The exit waves are calculated by
|
||||
shifting the probe by the rounded value of the translation
|
||||
|
||||
If multiple_modes is set to False, any additional dimensions in the
|
||||
ptycho_2D_round function will be assumed to correspond to the translation
|
||||
index. If multiple_modes is set to true, the (-4th) dimension of the probe
|
||||
will always be assumed to be defining a set of (P) incoherently mixing
|
||||
modes to be broadcast all translation indices. If any additional dimensions
|
||||
closer to the start exist, they will be assumed to be translation indices
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : torch.Tensor
|
||||
A (P)xMxL probe function to illuminate the object
|
||||
object : torch.Tensor
|
||||
The object function to be probed
|
||||
translations : torch.Tensor
|
||||
The (N)x2 array of (i,j) translations to simulate
|
||||
multuple_modes : bool
|
||||
Default False, whether to assume the probe contains multiple modes
|
||||
|
||||
Returns
|
||||
-------
|
||||
exit_waves : torch.Tensor
|
||||
An (N)x(P)xMxL tensor of the calculated exit waves
|
||||
"""
|
||||
|
||||
single_translation = False
|
||||
if translations.dim() == 1:
|
||||
translations = translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
|
||||
integer_translations = t.round(translations).to(dtype=t.int32)
|
||||
|
||||
if upsample_obj:
|
||||
selections = t.stack([obj[tr[0]:tr[0]+probe.shape[-2]//2,
|
||||
tr[1]:tr[1]+probe.shape[-1]//2]
|
||||
for tr in integer_translations])
|
||||
selections = image_processing.fourier_upsample(selections,
|
||||
preserve_mean=True)
|
||||
|
||||
else:
|
||||
selections = t.stack([obj[tr[0]:tr[0]+probe.shape[-2],
|
||||
tr[1]:tr[1]+probe.shape[-1]]
|
||||
for tr in integer_translations])
|
||||
|
||||
|
||||
if multiple_modes:
|
||||
# if the probe dimension is 4, then this hasn't yet been broadcast
|
||||
# over the translation dimensions
|
||||
output = probe * selections[:,None,:,:]
|
||||
else:
|
||||
output = probe * selections
|
||||
|
||||
if single_translation:
|
||||
return output[0]
|
||||
else:
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def ptycho_2D_linear(probe, obj, translations, shift_probe=True):
|
||||
"""Returns a stack of exit waves accounting for subpixel shifts
|
||||
|
||||
This function returns a collection of exit waves, with the first
|
||||
dimension as the translation index and the final dimensions
|
||||
corresponding to the detector. The exit waves are calculated by
|
||||
shifting the probe with each translation in turn, using linear
|
||||
interpolation to combine the results
|
||||
|
||||
If shift_probe is True, it applies the subpixel shift to the probe,
|
||||
otherwise the subpixel shift is applied to the object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : torch.Tensor
|
||||
An MxL probe function for the exit waves
|
||||
object : torch.Tensor
|
||||
The object function to be probed
|
||||
translations : torch.Tensor
|
||||
The Nx2 array of translations to simulate
|
||||
shift_probe : bool
|
||||
Default True, Whether to subpixel shift the probe or object
|
||||
|
||||
Returns
|
||||
-------
|
||||
exit_waves : torch.Tensor
|
||||
An NxMxL tensor of the calculated exit waves
|
||||
"""
|
||||
single_translation = False
|
||||
if translations.dim() == 1:
|
||||
translations = translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
# Separate the translations into a part that chooses the window
|
||||
# And a part that defines the windowing function
|
||||
integer_translations = t.floor(translations)
|
||||
subpixel_translations = translations - integer_translations
|
||||
integer_translations = integer_translations.to(dtype=t.int32)
|
||||
|
||||
exit_waves = []
|
||||
if shift_probe:
|
||||
for tr, sp in zip(integer_translations,
|
||||
subpixel_translations):
|
||||
# This isn't perfectly symmetric but I think it's okay for now
|
||||
# It should get the job done
|
||||
# Basically, we shift the probe's position by a subpixel (i,j),
|
||||
# rolling the edges of the array, and use that to multiply
|
||||
# by the object
|
||||
sel00 = probe[:,:]
|
||||
sel01 = t.cat((probe[:,-1:],probe[:,:-1]),dim=1)
|
||||
sel10 = t.cat((probe[-1:,:],probe[:-1,:]),dim=0)
|
||||
sel11 = t.cat((sel01[-1:,:],sel01[:-1,:]),dim=0)
|
||||
|
||||
selection = sel00 * (1-sp[0])*(1-sp[1]) + \
|
||||
sel10 * sp[0]*(1-sp[1]) + \
|
||||
sel01 * (1-sp[0])*sp[1] + \
|
||||
sel11 * sp[0]*sp[1]
|
||||
|
||||
obj_slice = obj[tr[0]:tr[0]+probe.shape[0],
|
||||
tr[1]:tr[1]+probe.shape[1]]
|
||||
|
||||
exit_waves.append(selection * obj_slice)
|
||||
else:
|
||||
for tr, sp in zip(integer_translations,
|
||||
subpixel_translations):
|
||||
#
|
||||
# Here we subpixel shift the object by (-i,-j) after
|
||||
# slicing out the correct translation of the probe
|
||||
#
|
||||
|
||||
sel00 = obj[tr[0]:tr[0]+probe.shape[0],
|
||||
tr[1]:tr[1]+probe.shape[1]]
|
||||
|
||||
sel01 = obj[tr[0]:tr[0]+probe.shape[0],
|
||||
tr[1]+1:tr[1]+1+probe.shape[1]]
|
||||
|
||||
sel10 = obj[tr[0]+1:tr[0]+1+probe.shape[0],
|
||||
tr[1]:tr[1]+probe.shape[1]]
|
||||
|
||||
sel11 = obj[tr[0]+1:tr[0]+1+probe.shape[0],
|
||||
tr[1]+1:tr[1]+1+probe.shape[1]]
|
||||
|
||||
selection = sel00 * (1-sp[0])*(1-sp[1]) + \
|
||||
sel01 * (1-sp[0])*sp[1] + \
|
||||
sel10 * sp[0]*(1-sp[1]) + \
|
||||
sel11 * sp[0]*sp[1]
|
||||
|
||||
exit_waves.append(probe * selection)
|
||||
|
||||
if single_translation:
|
||||
return exit_waves[0]
|
||||
else:
|
||||
return t.stack(exit_waves)
|
||||
|
||||
|
||||
def ptycho_2D_sinc(probe, obj, translations, shift_probe=True, padding=10, multiple_modes=True, polarized=False, polarizer=None, analyzer=None):
|
||||
"""Returns a stack of exit waves accounting for subpixel shifts
|
||||
|
||||
This function returns a collection of exit waves, with the first
|
||||
dimension as the translation index and the final dimensions
|
||||
corresponding to the detector. The exit waves are calculated by
|
||||
shifting the probe with each translation in turn, using sinc
|
||||
interpolation (done via multiplication with a complex exponential
|
||||
in Fourier space)
|
||||
|
||||
If shift_probe is True, it applies the subpixel shift to the probe,
|
||||
otherwise the subpixel shift is applied to the object [not yet implemented]
|
||||
|
||||
If multiple_modes is set to False, any additional dimensions in the
|
||||
ptycho_2D_round function will be assumed to correspond to the translation
|
||||
index. If multiple_modes is set to true, the (-4th) dimension of the probe
|
||||
will always be assumed to be defining a set of (P) incoherently mixing
|
||||
modes to be broadcast all translation indices. If any additional dimensions
|
||||
closer to the start exist, they will be assumed to be translation indices
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : torch.Tensor
|
||||
An (P)xMxL probe function for the exit waves
|
||||
object : torch.Tensor
|
||||
The object function to be probed
|
||||
translations : torch.Tensor
|
||||
The (N)x2 array of translations to simulate
|
||||
shift_probe : bool
|
||||
Default True, Whether to subpixel shift the probe or object
|
||||
multuple_modes : bool
|
||||
Default False, whether to assume the probe contains multiple modes
|
||||
|
||||
Returns
|
||||
-------
|
||||
exit_waves : torch.Tensor
|
||||
An (N)x(P)xMxL tensor of the calculated exit waves
|
||||
"""
|
||||
|
||||
single_translation = False
|
||||
if translations.dim() == 1:
|
||||
translations = translations[None, :]
|
||||
single_translation = True
|
||||
|
||||
# Separate the translations into a part that chooses the window
|
||||
# And a part that defines the windowing function
|
||||
integer_translations = t.floor(translations)
|
||||
subpixel_translations = translations - integer_translations
|
||||
integer_translations = integer_translations.to(dtype=t.int32)
|
||||
if not polarized:
|
||||
selections = t.stack([obj[tr[0]:tr[0]+probe.shape[-2],
|
||||
tr[1]:tr[1]+probe.shape[-1]]
|
||||
for tr in integer_translations])
|
||||
else:
|
||||
selections = t.stack([obj[:, :, tr[0]:tr[0]+probe.shape[-2],
|
||||
tr[1]:tr[1]+probe.shape[-1]]
|
||||
for tr in integer_translations])
|
||||
# Nx2x2xMxL tensor
|
||||
|
||||
exit_waves = []
|
||||
if shift_probe:
|
||||
i = t.arange(probe.shape[-2],device=probe.device,dtype=t.float32) \
|
||||
- probe.shape[-2]//2
|
||||
j = t.arange(probe.shape[-1],device=probe.device,dtype=t.float32) \
|
||||
- probe.shape[-1]//2
|
||||
I,J = t.meshgrid(i,j, indexing='ij')
|
||||
I = 2 * np.pi * I / probe.shape[-2]
|
||||
J = 2 * np.pi * J / probe.shape[-1]
|
||||
phase_masks = t.exp(1j*(-subpixel_translations[:,0,None,None]*I
|
||||
-subpixel_translations[:,1,None,None]*J))
|
||||
if polarized:
|
||||
phase_masks = phase_masks[..., None, :, :]
|
||||
# Nx2x1xMxL tensor
|
||||
# probe is (N)(P)x2xMxL tensor
|
||||
fft_probe = t.fft.fftshift(t.fft.fft2(probe),dim=(-1,-2))
|
||||
if multiple_modes: # Multi-mode probe
|
||||
if polarized:
|
||||
shifted_fft_probe = fft_probe * phase_masks[...,None,:,:,:]
|
||||
else:
|
||||
shifted_fft_probe = fft_probe * phase_masks[...,None,:,:]
|
||||
else:
|
||||
shifted_fft_probe = fft_probe * phase_masks
|
||||
shifted_probe = t.fft.ifft2(t.fft.ifftshift(shifted_fft_probe,
|
||||
dim=(-1,-2)))
|
||||
if not polarized:
|
||||
if multiple_modes: # Multi-mode probe
|
||||
output = shifted_probe * selections[...,None,:,:]
|
||||
else:
|
||||
output = shifted_probe * selections
|
||||
# selections: Nx2x2xMxL
|
||||
# probe: Nx(P)x2x1xMxL
|
||||
else:
|
||||
output = polarization.apply_jones_matrix(shifted_probe, selections, multiple_modes=multiple_modes)
|
||||
|
||||
else:
|
||||
raise NotImplementedError('Object shift not yet implemented')
|
||||
if single_translation:
|
||||
return output[0]
|
||||
else:
|
||||
return output
|
||||
|
||||
|
||||
def ptycho_2D_sinc_s_matrix(probe, s_matrix, translations, shift_probe=True, padding=10):
|
||||
"""Returns a stack of exit waves accounting for subpixel shifts
|
||||
|
||||
This function returns a collection of exit waves, with the first
|
||||
dimension as the translation index and the final dimensions
|
||||
corresponding to the detector. The exit waves are calculated by
|
||||
shifting the probe with each translation in turn, using sinc
|
||||
interpolation (done via multiplication with a complex exponential
|
||||
in Fourier space)
|
||||
|
||||
If shift_probe is True, it applies the subpixel shift to the probe,
|
||||
otherwise the subpixel shift is applied to the object
|
||||
|
||||
This needs to be edited to use a slightly different meaning of the s-wave
|
||||
format. Currently, each pixel in the latter two dimensions index a location
|
||||
on the input wavefield, and the first two indexes index differences from
|
||||
that pixel. It is easier to interpret the resulting matrix though if the
|
||||
latter two indices index locations in the output plane. NOTE: I believe
|
||||
this change has now been made
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : torch.Tensor
|
||||
An MxL probe function for the exit waves
|
||||
s_matrix : torch.Tensor
|
||||
The 4D S-Matrix tensor (2B+1x2B+1xObject Shape) to be probed
|
||||
translations : torch.Tensor
|
||||
The Nx2 array of translations to simulate
|
||||
shift_probe : bool
|
||||
Default True, Whether to subpixel shift the probe or object
|
||||
padding : int
|
||||
Default 10, if shifting the object, the padding to apply to the object to avoid circular shift effects
|
||||
|
||||
Returns
|
||||
-------
|
||||
exit_waves : torch.Tensor
|
||||
An NxMxL tensor of the calculated exit waves
|
||||
"""
|
||||
single_translation = False
|
||||
if translations.dim() == 1:
|
||||
translations = translations[None,:]
|
||||
single_translation = True
|
||||
|
||||
# Separate the translations into a part that chooses the window
|
||||
# And a part that defines the windowing function
|
||||
integer_translations = t.floor(translations)
|
||||
subpixel_translations = translations - integer_translations
|
||||
integer_translations = integer_translations.to(dtype=t.int32)
|
||||
|
||||
exit_waves = []
|
||||
|
||||
B = s_matrix.shape[0]//2
|
||||
|
||||
if shift_probe:
|
||||
i = t.arange(probe.shape[-2]) - probe.shape[-2]//2
|
||||
j = t.arange(probe.shape[-1]) - probe.shape[-1]//2
|
||||
I,J = t.meshgrid(i,j, indexing='ij')
|
||||
I = 2 * np.pi * I.to(t.float32) / probe.shape[-2]
|
||||
J = 2 * np.pi * J.to(t.float32) / probe.shape[-1]
|
||||
I = I.to(dtype=probe.dtype,device=probe.device)
|
||||
J = J.to(dtype=probe.dtype,device=probe.device)
|
||||
|
||||
for tr, sp in zip(integer_translations,
|
||||
subpixel_translations):
|
||||
fft_probe = t.fft.fftshift(t.fft.fft2(probe), dim=(-1,-2))
|
||||
shifted_fft_probe = fft_probe * t.exp(1j*(-sp[0]*I - sp[1]*J))
|
||||
shifted_probe = t.fft.ifft2(t.fft.ifftshift(shifted_fft_probe,
|
||||
dim=(-1,-2)))
|
||||
|
||||
s_matrix_slice = s_matrix[:,:,tr[0]:tr[0]+probe.shape[-2]+2*B,
|
||||
tr[1]:tr[1]+probe.shape[-1]+2*B]
|
||||
|
||||
|
||||
output = t.zeros([probe.shape[-2]+2*B,probe.shape[-1]+2*B,2]).to(
|
||||
device=s_matrix_slice.device,
|
||||
dtype=s_matrix_slice.dtype)
|
||||
|
||||
|
||||
for i in range(s_matrix.shape[0]):
|
||||
for j in range(s_matrix.shape[1]):
|
||||
output [i:i+probe.shape[-2],j:j+probe.shape[-1]] += \
|
||||
shifted_probe * s_matrix_slice[i,j,i:i+probe.shape[-2],j:j+probe.shape[-1]]
|
||||
|
||||
exit_waves.append(output)
|
||||
|
||||
else:
|
||||
raise NotImplementedError('Object shift not yet implemented')
|
||||
|
||||
if single_translation:
|
||||
return exit_waves[0]
|
||||
else:
|
||||
return t.stack(exit_waves)
|
||||
|
||||
|
||||
def RPI_interaction(probe, obj):
|
||||
"""Returns an exit wave from a high-res probe and a low-res obj
|
||||
|
||||
In this interaction, the probe and object arrays are assumed to cover
|
||||
the same physical region of space, but with the probe array sampling that
|
||||
region of space more finely. Thus, to do the interaction, the object
|
||||
is first upsampled by padding it in Fourier space (equivalent to a sinc
|
||||
interpolation) before being multiplied with the probe. This function is
|
||||
called RPI_interaction because this interaction is central to the RPI
|
||||
method and is not commonly used elsewhere.
|
||||
|
||||
This also works with object functions that have an extra first dimension
|
||||
for an incoherently mixing model.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
probe : torch.Tensor
|
||||
An MxL probe function for simulating the exit waves
|
||||
obj : torch.Tensor
|
||||
An M'xL' or NxM'xL' object function for simulating the exit waves
|
||||
|
||||
Returns
|
||||
-------
|
||||
exit_wave : torch.Tensor
|
||||
An MxL tensor of the calculated exit waves
|
||||
"""
|
||||
|
||||
# TODO: The upsampling only works for arrays of even dimension!
|
||||
|
||||
# The far-field propagator is just a 2D FFT but with an fftshift
|
||||
fftobj = propagators.far_field(obj)
|
||||
# We calculate the padding that we need to do the upsampling
|
||||
# This is carefully set up to keep the zero-frequency pixel in the correct
|
||||
# location as the overall shape changes. Don't mess with this without
|
||||
# having thought about this carefully.
|
||||
pad2l = probe.shape[-2]//2 - obj.shape[-2]//2
|
||||
pad2r = probe.shape[-2] - obj.shape[-2] - pad2l
|
||||
pad1l = probe.shape[-1]//2 - obj.shape[-1]//2
|
||||
pad1r = probe.shape[-1] - obj.shape[-1] - pad1l
|
||||
|
||||
fftobj = t.nn.functional.pad(fftobj, (pad1l, pad1r, pad2l, pad2r))
|
||||
|
||||
# Again, just an inverse FFT but with an fftshift
|
||||
upsampled_obj = propagators.inverse_far_field(fftobj)
|
||||
|
||||
if obj.dim() >= 3:
|
||||
return probe[None,...] * upsampled_obj
|
||||
else:
|
||||
return probe * upsampled_obj
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.losses.losses import *
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Contains various loss functions to be used for optimization
|
||||
|
||||
It exposes three losses, one returning the mean squared amplitude error, one
|
||||
that returns the mean squared intensity error, and one that returns the
|
||||
maximum likelihood metric for a system with Poisson statistics.
|
||||
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
|
||||
__all__ = ['amplitude_mse', 'intensity_mse', 'poisson_nll']
|
||||
|
||||
|
||||
def amplitude_mse(intensities, sim_intensities, mask=None):
|
||||
""" Returns the mean squared error of a simulated dataset's amplitudes
|
||||
|
||||
Calculates the mean squared error between a given set of
|
||||
measured diffraction intensities and a simulated set.
|
||||
|
||||
This function calculates the mean squared error between their
|
||||
associated amplitudes. Because this is not well defined for negative
|
||||
numbers, make sure that all the intensities are >0 before using this
|
||||
loss. Note that this is actually a sum-squared error, because this
|
||||
formulation makes it vastly simpler to compare error calculations
|
||||
between reconstructions with different minibatch size. I hope to
|
||||
find a better way to do this that is more honest with this
|
||||
cost function, though.
|
||||
|
||||
It can accept intensity and simulated intensity tensors of any shape
|
||||
as long as their shapes match, and the provided mask array can be
|
||||
broadcast correctly along them.
|
||||
|
||||
This is empirically the most useful loss function for most cases
|
||||
|
||||
Parameters
|
||||
----------
|
||||
intensities : torch.Tensor
|
||||
A tensor with measured detector values
|
||||
sim_intensities : torch.Tensor
|
||||
A tensor of simulated detector intensities
|
||||
mask : torch.Tensor
|
||||
A mask with ones for pixels to include and zeros for pixels to exclude
|
||||
|
||||
Returns
|
||||
-------
|
||||
loss : torch.Tensor
|
||||
A single value for the mean amplitude mse
|
||||
"""
|
||||
|
||||
# I know it would be more efficient if this function took in the
|
||||
# amplitudes instead of the intensities, but I want to be consistent
|
||||
# with all the errors working off of the same inputs
|
||||
|
||||
if mask is None:
|
||||
return t.sum((t.sqrt(sim_intensities) -
|
||||
t.sqrt(intensities))**2)
|
||||
else:
|
||||
masked_intensities = intensities.masked_select(mask)
|
||||
return t.sum((t.sqrt(sim_intensities.masked_select(mask)) -
|
||||
t.sqrt(masked_intensities))**2)
|
||||
|
||||
|
||||
|
||||
def intensity_mse(intensities, sim_intensities, mask=None):
|
||||
""" Returns the mean squared error of a simulated dataset's intensities
|
||||
|
||||
Calculates the summed mean squared error between a given set of
|
||||
diffraction intensities - the measured set of detector intensities -
|
||||
and a simulated set of diffraction intensities. This function
|
||||
calculates the mean squared error between the intensities.
|
||||
|
||||
It can accept intensity and simulated intensity tensors of any shape
|
||||
as long as their shapes match, and the provided mask array can be
|
||||
broadcast correctly along them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
intensities : torch.Tensor
|
||||
A tensor with measured detector intensities.
|
||||
sim_intensities : torch.Tensor
|
||||
A tensor of simulated detector intensities
|
||||
mask : torch.Tensor
|
||||
A mask with ones for pixels to include and zeros for pixels to exclude
|
||||
|
||||
Returns
|
||||
-------
|
||||
loss : torch.Tensor
|
||||
A single value for the mean intensity mse
|
||||
|
||||
"""
|
||||
if mask is None:
|
||||
return t.sum((sim_intensities - intensities)**2) \
|
||||
/ intensities.view(-1).shape[0]
|
||||
else:
|
||||
masked_intensities = intensities.masked_select(mask)
|
||||
return t.sum((sim_intensities.masked_select(mask) -
|
||||
masked_intensities)**2) \
|
||||
/ masked_intensities.shape[0]
|
||||
|
||||
|
||||
|
||||
def poisson_nll(intensities, sim_intensities, mask=None, eps=1e-6, subtract_min=False):
|
||||
""" Returns the Poisson negative log likelihood for a simulated dataset's intensities
|
||||
|
||||
Calculates the overall Poisson maximum likelihood metric using
|
||||
diffraction intensities - the measured set of detector intensities -
|
||||
and a simulated set of intensities. This loss would be appropriate
|
||||
for detectors in a single-photon counting mode, with their output
|
||||
scaled to number of photons
|
||||
|
||||
Note that this calculation ignores the log(intensities!) term in the
|
||||
full expression for Poisson negative log likelihood. This term doesn't
|
||||
change the calculated gradients so isn't worth taking the time to compute
|
||||
|
||||
It can accept intensity and simulated intensity tensors of any shape
|
||||
as long as their shapes match, and the provided mask array can be
|
||||
broadcast correctly along them.
|
||||
|
||||
The default value of eps is 1e-6 - a nonzero value here helps avoid
|
||||
divergence of the log function near zero.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
intensities : torch.Tensor
|
||||
A tensor with measured detector intensities.
|
||||
sim_intensities : torch.Tensor
|
||||
A tensor of simulated detector intensities
|
||||
mask : torch.Tensor
|
||||
A mask with ones for pixels to include and zeros for pixels to exclude
|
||||
eps : float
|
||||
Optional, a small number to add to the simulated intensities
|
||||
|
||||
Returns
|
||||
-------
|
||||
loss : torch.Tensor
|
||||
A single value for the poisson negative log likelihood
|
||||
|
||||
"""
|
||||
#When x.logy gets into the regular build, add it by uncommenting!
|
||||
if mask is None:
|
||||
|
||||
nll = t.sum(sim_intensities+eps -
|
||||
intensities * t.log(sim_intensities+eps)) \
|
||||
/ intensities.view(-1).shape[0]
|
||||
#nll = t.sum(sim_intensities+epsa -
|
||||
# t.xlogy(intensities,sim_intensities+eps)) \
|
||||
# / intensities.view(-1).shape[0]
|
||||
if subtract_min:
|
||||
nll -= t.nansum(intensities - intensities*t.log(intensities))\
|
||||
/ intensities.view(-1).shape[0]
|
||||
#nll -= t.sum(intensities - t.xlogy(intensities,intensities))
|
||||
# We don't need to include the log factorial part here, because
|
||||
# it will get subtracted off in the min anyway.
|
||||
|
||||
return nll
|
||||
else:
|
||||
masked_intensities = intensities.masked_select(mask)
|
||||
masked_sims = sim_intensities.masked_select(mask)
|
||||
|
||||
nll = t.sum(masked_sims + eps - \
|
||||
masked_intensities * t.log(masked_sims+eps)) \
|
||||
/ masked_intensities.shape[0]
|
||||
|
||||
#nll = t.sum(masked_sims + eps - \
|
||||
# t.xlogy(masked_intensities, masked_sims+eps)) \
|
||||
# / masked_intensities.shape[0]
|
||||
|
||||
|
||||
if subtract_min:
|
||||
nll -= t.nansum(masked_intensities -
|
||||
masked_intensities*t.log(masked_intensities)) \
|
||||
/ masked_intensities.shape[0]
|
||||
|
||||
return nll
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.measurements.measurements import *
|
||||
@@ -0,0 +1,159 @@
|
||||
"""This module contains tools to simulate various measurement models
|
||||
|
||||
All the measurements here are safe to use in an automatic differentiation
|
||||
model. There exist tools to simulate detectors with finite saturation
|
||||
thresholds, backgrounds, and more.
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
import numpy as np
|
||||
from torch.nn.functional import avg_pool2d
|
||||
|
||||
#
|
||||
# This file will host tools to turn a propagated wavefield into a measured
|
||||
# intensity pattern on a detector
|
||||
#
|
||||
|
||||
__all__ = ['intensity', 'incoherent_sum', 'quadratic_background']
|
||||
|
||||
|
||||
def intensity(wavefield, detector_slice=None, epsilon=1e-7, saturation=None, oversampling=1):
|
||||
"""Returns the intensity of a wavefield
|
||||
|
||||
The intensity is defined as the magnitude squared of the
|
||||
wavefront. If a detector slice is given, the returned array
|
||||
will only include that slice from the simulated wavefront.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefield : torch.Tensor
|
||||
A JxMxN stack of complex-valued wavefields
|
||||
detector_slice : slice
|
||||
Optional, a slice or tuple of slices defining a section of the simulation to return
|
||||
saturation : float
|
||||
Optional, a maximum saturation value to clamp the resulting intensities to
|
||||
oversampling : int
|
||||
Default 1, the width of the region pixels in the wavefield to bin into a single detector pixel
|
||||
|
||||
Returns
|
||||
-------
|
||||
sim_patterns : torch.Tensor
|
||||
A real MxN array storing the wavefield's intensities
|
||||
"""
|
||||
output = t.abs(wavefield)**2
|
||||
|
||||
# Now we apply oversampling
|
||||
if oversampling != 1:
|
||||
if wavefield.dim() == 2:
|
||||
output = avg_pool2d(output.unsqueeze(0), oversampling)[0]
|
||||
else:
|
||||
output = avg_pool2d(output, oversampling)
|
||||
|
||||
# Then we grab the detector slice
|
||||
if detector_slice is not None:
|
||||
if wavefield.dim() == 2:
|
||||
output = output[detector_slice]
|
||||
else:
|
||||
output = output[(np.s_[:],) + detector_slice]
|
||||
|
||||
# And now saturation
|
||||
if saturation is None:
|
||||
return output + epsilon
|
||||
else:
|
||||
return t.clamp(output + epsilon,0,saturation)
|
||||
|
||||
|
||||
def incoherent_sum(wavefields, detector_slice=None, epsilon=1e-7, saturation=None, oversampling=1):
|
||||
"""Returns the incoherent sum of the intensities of the wavefields
|
||||
|
||||
The intensity is defined as the sum of the magnitudes squared of
|
||||
the wavefields. If a detector slice is given, the returned array
|
||||
will only include that slice from the simulated wavefronts.
|
||||
|
||||
The (-3) index is the set of incoherently adding patterns, and any
|
||||
indexes further to the front correspond to the set of diffraction patterns
|
||||
to measure. The (-2) and (-1) indices are the wavefield, and the final
|
||||
index is the complex index
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefields : torch.Tensor
|
||||
An LxJxMxNx stack of complex wavefields
|
||||
detector_slice : slice
|
||||
Optional, a slice or tuple of slices defining a section of the simulation to return
|
||||
saturation : float
|
||||
Optional, a maximum saturation value to clamp the resulting intensities to
|
||||
oversampling : int
|
||||
Default 1, the width of the region pixels in the wavefield to bin into a single detector pixel
|
||||
|
||||
Returns
|
||||
-------
|
||||
sim_patterns : torch.Tensor
|
||||
A real LXMxN array storing the incoherently summed intensities
|
||||
"""
|
||||
|
||||
output = t.sum(t.abs(wavefields)**2,dim=-3)
|
||||
|
||||
# Now we apply oversampling
|
||||
if oversampling != 1:
|
||||
if wavefields.dim() == 3:
|
||||
output = avg_pool2d(output.unsqueeze(0), oversampling)[0]
|
||||
else:
|
||||
output = avg_pool2d(output, oversampling)
|
||||
|
||||
# Then we grab the detector slice
|
||||
if detector_slice is not None:
|
||||
if wavefields.dim() == 3:
|
||||
output = output[detector_slice]
|
||||
else:
|
||||
output = output[(np.s_[:],) + detector_slice]
|
||||
|
||||
if saturation is None:
|
||||
return output + epsilon
|
||||
else:
|
||||
return t.clamp(output + epsilon,0,saturation)
|
||||
|
||||
|
||||
def quadratic_background(wavefield, background, *args, detector_slice=None, measurement=intensity, epsilon=1e-7, saturation=None, oversampling=1):
|
||||
"""Returns the intensity of a wavefield plus a background
|
||||
|
||||
The intensity is calculated via the given measurment function
|
||||
Then, the square of the given background is added. This kind
|
||||
of background model is commonly used to enforce positivity of the
|
||||
background model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefield : torch.Tensor
|
||||
A JxMxN stack of complex-valued wavefields
|
||||
background : torch.Tensor
|
||||
An tensor storing the square root of the detector background
|
||||
detector_slice : slice
|
||||
Optional, a slice or tuple of slices defining a section of the simulation to return
|
||||
measurement : function
|
||||
Default is measurements.intensity, the measurement function to use.
|
||||
saturation : float
|
||||
Optional, a maximum saturation value to clamp the resulting intensities to
|
||||
oversampling : int
|
||||
Default 1, the width of the region pixels in the wavefield to bin into a single detector pixel
|
||||
|
||||
Returns
|
||||
-------
|
||||
sim_patterns : torch.Tensor
|
||||
A real MxN array storing the wavefield's intensities
|
||||
"""
|
||||
|
||||
if detector_slice is None:
|
||||
output = measurement(wavefield, *args, epsilon=epsilon,
|
||||
oversampling=oversampling) + background**2
|
||||
else:
|
||||
output = measurement(wavefield, *args, detector_slice=detector_slice,
|
||||
epsilon=epsilon, oversampling=oversampling) \
|
||||
+ background**2
|
||||
|
||||
# This has to be done after the background is added, hence we replicate
|
||||
# it here
|
||||
if saturation is None:
|
||||
return output
|
||||
else:
|
||||
return t.clamp(output,0,saturation)
|
||||
@@ -0,0 +1,2 @@
|
||||
from CDTools.tools.plotting.plotting import *
|
||||
|
||||
@@ -0,0 +1,782 @@
|
||||
"""This module contains functions for plotting various important metrics
|
||||
|
||||
All the plotting functions here can accept torch input or numpy input,
|
||||
to facilitate their use both for live inspection of running reconstructions
|
||||
and for after-the-fact analysis. Utilities for plotting complex valued
|
||||
images exist, as well as plotting scan patterns and nanomaps
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import hsv_to_rgb
|
||||
from matplotlib.widgets import Slider
|
||||
from matplotlib import ticker, patheffects
|
||||
|
||||
|
||||
__all__ = ['colorize', 'plot_amplitude', 'plot_phase',
|
||||
'plot_colorized', 'plot_translations', 'get_units_factor',
|
||||
'plot_nanomap', 'plot_real', 'plot_imag',
|
||||
'plot_nanomap_with_images']
|
||||
|
||||
|
||||
def colorize(z):
|
||||
""" Returns RGB values for a complex color plot given a complex array
|
||||
This function returns a set of RGB values that can be used directly
|
||||
in a call to imshow based on an input complex numpy array (not a
|
||||
torch tensor representing a complex field)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
z : array
|
||||
A complex-valued array
|
||||
Returns
|
||||
-------
|
||||
rgb : list(array)
|
||||
A list of arrays for the R,G, and B channels of an image
|
||||
"""
|
||||
|
||||
amp = np.abs(z)
|
||||
rmin = 0
|
||||
rmax = np.max(amp)
|
||||
amp = np.where(amp < rmin, rmin, amp)
|
||||
amp = np.where(amp > rmax, rmax, amp)
|
||||
ph = np.angle(z, deg=1) + 90
|
||||
# HSV are values in range [0,1]
|
||||
h = (ph % 360) / 360
|
||||
s = 0.85 * np.ones_like(h)
|
||||
v = (amp - rmin) / (rmax - rmin)
|
||||
|
||||
return hsv_to_rgb(np.dstack((h,s,v)))
|
||||
|
||||
|
||||
def get_units_factor(units):
|
||||
"""Gets the multiplicative factor associated with a length unit
|
||||
|
||||
Parameters
|
||||
----------
|
||||
units : str
|
||||
The abbreviation for the unit type
|
||||
|
||||
Returns
|
||||
-------
|
||||
factor : float
|
||||
The factor meters / (unit)
|
||||
"""
|
||||
|
||||
u = units.lower()
|
||||
if u=='m':
|
||||
factor=1
|
||||
if u=='cm':
|
||||
factor=1e2
|
||||
if u=='mm':
|
||||
factor=1e3
|
||||
if u=='um' or u=="$\\mu$m":
|
||||
factor=1e6
|
||||
if u=='nm':
|
||||
factor=1e9
|
||||
if u=='a':
|
||||
factor=1e10
|
||||
if u=='pm':
|
||||
factor=1e12
|
||||
return factor
|
||||
|
||||
|
||||
def plot_image(im, plot_func=lambda x: x, fig=None, basis=None, units='$\\mu$m', cmap='viridis', cmap_label=None, interpolation=None, **kwargs):
|
||||
"""Plots an image with a colorbar and on an appropriate spatial grid
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates
|
||||
|
||||
Finally, if a function is passed to the plot_func argument, this function
|
||||
will be called on each slice of data before it is plotted. This is used
|
||||
internally to enable the plot_real, plot_image, plot_phase, etc. functions.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
plot_func : callable
|
||||
A function which maps numpy arrays to the image to be plotted
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
cmap : str
|
||||
Default is 'viridis', the colormap to plot with
|
||||
cmap_label : str
|
||||
What to label the colorbar when plotting
|
||||
interpolation : str
|
||||
What interpolation to use for imshow
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
|
||||
# convert to numpy
|
||||
if isinstance(im, t.Tensor):
|
||||
# If final dimension is 2, assume it is a complex array. If not,
|
||||
# assume it represents a real array
|
||||
if im.shape[-1] == 2:
|
||||
im = im.detach().cpu().numpy()
|
||||
else:
|
||||
im = im.detach().cpu().numpy()
|
||||
|
||||
if fig is None:
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111, **kwargs)
|
||||
|
||||
# This nukes everything and updates either the appropriate image from the
|
||||
# stack of images, or the only image if only a single image has been
|
||||
# given
|
||||
def make_plot(idx):
|
||||
plt.figure(fig.number)
|
||||
title = plt.gca().get_title()
|
||||
fig.clear()
|
||||
|
||||
|
||||
# If im only has two dimensions, this reshape will add a leading
|
||||
# dimension, and update will be called on index 0. If it has 3 or more
|
||||
# dimensions, then all the leading dimensions will be compressed into
|
||||
# one long dimension which can be scrolled through.
|
||||
s = im.shape
|
||||
reshaped_im = im.reshape(-1,s[-2],s[-1])
|
||||
num_images = reshaped_im.shape[0]
|
||||
fig.plot_idx = idx % num_images
|
||||
|
||||
to_plot = plot_func(reshaped_im[fig.plot_idx])
|
||||
|
||||
#Plot in a basis if it exists, otherwise dont
|
||||
if basis is not None:
|
||||
if isinstance(basis,t.Tensor):
|
||||
np_basis = basis.detach().cpu().numpy()
|
||||
else:
|
||||
np_basis = basis
|
||||
# This fails if the basis is not rectangular
|
||||
basis_norm = np.linalg.norm(np_basis, axis = 0)
|
||||
basis_norm = basis_norm * get_units_factor(units)
|
||||
|
||||
extent = [0, im.shape[-1]*basis_norm[1], 0,
|
||||
im.shape[-2]*basis_norm[0]]
|
||||
else:
|
||||
extent=None
|
||||
|
||||
plt.imshow(to_plot, cmap = cmap, extent = extent, interpolation=interpolation)
|
||||
cbar = plt.colorbar()
|
||||
if cmap_label is not None:
|
||||
cbar.set_label(cmap_label)
|
||||
|
||||
if basis is not None:
|
||||
plt.xlabel('X (' + units + ')')
|
||||
plt.ylabel('Y (' + units + ')')
|
||||
else:
|
||||
plt.xlabel('j (pixels)')
|
||||
plt.ylabel('i (pixels)')
|
||||
|
||||
|
||||
plt.title(title)
|
||||
|
||||
if len(im.shape) >= 3:
|
||||
plt.text(0.03, 0.03, str(fig.plot_idx), fontsize=14, transform=plt.gcf().transFigure)
|
||||
return fig
|
||||
|
||||
if hasattr(fig, 'plot_idx'):
|
||||
result = make_plot(fig.plot_idx)
|
||||
else:
|
||||
result = make_plot(0)
|
||||
|
||||
update = make_plot
|
||||
|
||||
|
||||
def on_action(event):
|
||||
if not hasattr(event, 'button'):
|
||||
event.button = None
|
||||
if not hasattr(event, 'key'):
|
||||
event.key = None
|
||||
|
||||
if event.key == 'up' or event.button == 'up':
|
||||
update(fig.plot_idx - 1)
|
||||
elif event.key == 'down' or event.button == 'down':
|
||||
update(fig.plot_idx + 1)
|
||||
plt.draw()
|
||||
|
||||
if len(im.shape) >=3:
|
||||
if not hasattr(fig,'my_callbacks'):
|
||||
fig.my_callbacks = []
|
||||
|
||||
for cid in fig.my_callbacks:
|
||||
fig.canvas.mpl_disconnect(cid)
|
||||
fig.my_callbacks = []
|
||||
fig.my_callbacks.append(fig.canvas.mpl_connect('key_press_event',on_action))
|
||||
fig.my_callbacks.append(fig.canvas.mpl_connect('scroll_event',on_action))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def plot_real(im, fig = None, basis=None, units='$\\mu$m', cmap='viridis', cmap_label='Real Part (a.u.)', **kwargs):
|
||||
"""Plots the real part of a complex array with dimensions NxM
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
cmap : str
|
||||
Default is 'viridis', the colormap to plot with
|
||||
cmap_label : str
|
||||
What to label the colorbar when plotting
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
plot_func = lambda x: np.real(x)
|
||||
return plot_image(im, plot_func=plot_func, fig=fig, basis=basis,
|
||||
units=units, cmap=cmap, cmap_label=cmap_label,
|
||||
**kwargs)
|
||||
|
||||
|
||||
|
||||
def plot_imag(im, fig = None, basis=None, units='$\\mu$m', cmap='viridis', cmap_label='Imaginary Part (a.u.)', **kwargs):
|
||||
"""Plots the imaginary part of a complex array with dimensions NxM
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
cmap : str
|
||||
Default is 'viridis', the colormap to plot with
|
||||
cmap_label : str
|
||||
What to label the colorbar when plotting
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
plot_func = lambda x: np.imag(x)
|
||||
return plot_image(im, plot_func=plot_func, fig=fig, basis=basis,
|
||||
units=units, cmap=cmap, cmap_label=cmap_label,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def plot_amplitude(im, fig = None, basis=None, units='$\\mu$m', cmap='viridis', cmap_label='Amplitude (a.u.)', **kwargs):
|
||||
"""Plots the amplitude of a complex array with dimensions NxM
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
cmap : str
|
||||
Default is 'viridis', the colormap to plot with
|
||||
cmap_label : str
|
||||
What to label the colorbar when plotting
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
plot_func = lambda x: np.absolute(x)
|
||||
return plot_image(im, plot_func=plot_func, fig=fig, basis=basis,
|
||||
units=units, cmap=cmap, cmap_label=cmap_label,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def plot_phase(im, fig=None, basis=None, units='$\\mu$m', cmap='auto', cmap_label='Phase (rad)', **kwargs):
|
||||
""" Plots the phase of a complex array with dimensions NxMx2
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
cmap : str
|
||||
Default is 'viridis', the colormap to plot with
|
||||
cmap_label : str
|
||||
What to label the colorbar when plotting
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
if cmap == 'auto':
|
||||
if 'twilight' in plt.colormaps():
|
||||
cmap = 'twilight'
|
||||
elif 'hsv' in plt.colormaps():
|
||||
cmap = 'hsv'
|
||||
else:
|
||||
raise AttributeError('Neither twilight or hsv colormap exists in this screwed up matplotlib install')
|
||||
|
||||
plot_func = lambda x: np.angle(x)
|
||||
return plot_image(im, plot_func=plot_func, fig=fig, basis=basis,
|
||||
units=units, cmap=cmap, cmap_label=cmap_label,
|
||||
**kwargs)
|
||||
|
||||
|
||||
def plot_amplitude_surfacenorm():
|
||||
pass
|
||||
|
||||
def plot_colorized(im, fig=None, basis=None, units='$\\mu$m', **kwargs):
|
||||
""" Plots the colorized version of a complex array with dimensions NxM
|
||||
|
||||
The darkness corresponds to the intensity of the image, and the color
|
||||
corresponds to the phase.
|
||||
|
||||
If a figure is given explicitly, it will clear that existing figure and
|
||||
plot over it. Otherwise, it will generate a new figure.
|
||||
|
||||
If a basis is explicitly passed, the image will be plotted in real-space
|
||||
coordinates
|
||||
|
||||
Parameters
|
||||
----------
|
||||
im : array
|
||||
An complex array with dimensions NxM
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
basis : np.array
|
||||
Optional, the 3x2 probe basis
|
||||
units : str
|
||||
The length units to mark on the plot, default is um
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
plot_func = lambda x: colorize(x)
|
||||
return plot_image(im, plot_func=plot_func, fig=fig, basis=basis,
|
||||
units=units, **kwargs)
|
||||
|
||||
|
||||
def plot_translations(translations, fig=None, units='$\\mu$m', lines=True, invert_xaxis=True, **kwargs):
|
||||
"""Plots a set of probe translations in a nicely formatted way
|
||||
|
||||
Parameters
|
||||
----------
|
||||
translations : array
|
||||
An Nx2 or Nx3 set of translations in real space
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
units : str
|
||||
Default is um, units to report in (assuming input in m)
|
||||
lines : bool
|
||||
Whether to plot lines indicating the path taken
|
||||
invert_xaxis : bool
|
||||
Default is True. This flips the x axis to match the convention from .cxi files of viewing the image from the beam's perspective
|
||||
\\**kwargs
|
||||
All other args are passed to fig.add_subplot(111, \\**kwargs)
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
|
||||
factor = get_units_factor(units)
|
||||
|
||||
if fig is None:
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(111, **kwargs)
|
||||
else:
|
||||
plt.figure(fig.number)
|
||||
plt.gcf().clear()
|
||||
|
||||
if isinstance(translations, t.Tensor):
|
||||
translations = translations.detach().cpu().numpy()
|
||||
|
||||
translations = translations * factor
|
||||
plt.plot(translations[:,0], translations[:,1],'k.')
|
||||
if invert_xaxis:
|
||||
plt.gca().invert_xaxis()
|
||||
|
||||
if lines:
|
||||
plt.plot(translations[:,0], translations[:,1],'b-', linewidth=0.5)
|
||||
plt.xlabel('X (' + units + ')')
|
||||
plt.ylabel('Y (' + units + ')')
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def plot_nanomap(translations, values, fig=None, units='$\\mu$m', convention='probe', invert_xaxis=True):
|
||||
"""Plots a set of nanomap data in a flexible way
|
||||
|
||||
Parameters
|
||||
----------
|
||||
translations : array
|
||||
An Nx2 or Nx3 set of translations in real space
|
||||
values : array
|
||||
A length-N object of values associated with the translations
|
||||
fig : matplotlib.figure.Figure
|
||||
Default is a new figure, a matplotlib figure to use to plot
|
||||
units : str
|
||||
Default is um, units to report in (assuming input in m)
|
||||
convention : str
|
||||
Default is 'probe', alternative is 'obj'. Whether the translations refer to the probe or object.
|
||||
invert_xaxis : bool
|
||||
Default is True. This flips the x axis to match the convention from .cxi files of viewing the image from the beam's perspective
|
||||
|
||||
Returns
|
||||
-------
|
||||
used_fig : matplotlib.figure.Figure
|
||||
The figure object that was actually plotted to.
|
||||
"""
|
||||
|
||||
if fig is None:
|
||||
fig = plt.figure()
|
||||
else:
|
||||
plt.figure(fig.number)
|
||||
plt.gcf().clear()
|
||||
|
||||
factor = get_units_factor(units)
|
||||
|
||||
bbox = fig.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
||||
if isinstance(translations, t.Tensor):
|
||||
trans = translations.detach().cpu().numpy()
|
||||
else:
|
||||
trans = np.array(translations)
|
||||
|
||||
if isinstance(values, t.Tensor):
|
||||
values = values.detach().cpu().numpy()
|
||||
else:
|
||||
values = np.array(values)
|
||||
|
||||
if convention.lower() != 'probe':
|
||||
trans = trans * -1
|
||||
|
||||
s = bbox.width * bbox.height / trans.shape[0] * 72**2 #72 is points per inch
|
||||
s /= 4 # A rough value to make the size work out
|
||||
|
||||
plt.scatter(factor * trans[:,0],factor * trans[:,1],s=s,c=values)
|
||||
if invert_xaxis:
|
||||
plt.gca().invert_xaxis()
|
||||
|
||||
plt.gca().set_facecolor('k')
|
||||
plt.xlabel('Translation x (' + units + ')')
|
||||
plt.ylabel('Translation y (' + units + ')')
|
||||
plt.colorbar()
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
def plot_nanomap_with_images(translations, get_image_func, values=None, mask=None, basis=None, fig=None, nanomap_units='$\\mu$m', image_units='$\\mu$m', convention='probe', image_title='Image', image_colorbar_title='Image Amplitude', nanomap_colorbar_title='Integrated Intensity', cmap='viridis', **kwargs):
|
||||
"""Plots a nanomap, with an image or stack of images for each point
|
||||
|
||||
In many situations, ptychography data or the output of ptychography
|
||||
reconstructions is formatted as a set of images associated with various
|
||||
points in real space. This function is designed to allow for browsing
|
||||
through this kind of data, by making it possible to visualize a
|
||||
|
||||
"""
|
||||
|
||||
# This should pull heavily from the dataset.inspect function
|
||||
# In fact, I should be able to replace most of that function with a
|
||||
# call to this function once it's built
|
||||
# We start by making the figure and axes
|
||||
|
||||
# The key will be writing this so it works okay when called in "update"
|
||||
# mode, i.e. on a figure that already has this thing showing.
|
||||
|
||||
if fig is None:
|
||||
fig = plt.figure(figsize=(8,5.3))
|
||||
else:
|
||||
plt.figure(fig.number)
|
||||
plt.gcf().clear()
|
||||
if hasattr(fig, 'nanomap_cids'):
|
||||
for cid in fig.nanomap_cids:
|
||||
fig.canvas.mpl_disconnect(cid)
|
||||
|
||||
# Does figsize work with the fig.subplots, or just for plt.subplots?
|
||||
axes = fig.subplots(1,2)
|
||||
|
||||
fig.tight_layout(rect=[0.04, 0.09, 0.98, 0.96])
|
||||
plt.subplots_adjust(wspace=0.25) #avoids overlap of labels with plots
|
||||
axslider = plt.axes([0.15,0.06,0.75,0.03])
|
||||
|
||||
# This gets the set of sizes for the points in the nanomap
|
||||
def calculate_sizes(idx):
|
||||
bbox = axes[0].get_window_extent().transformed(fig.dpi_scale_trans.inverted())
|
||||
s0 = bbox.width * bbox.height / translations.shape[0] * 72**2 #72 is points per inch
|
||||
s0 /= 4 # A rough value to make the size work out
|
||||
s = np.ones(translations.shape[0]) * s0
|
||||
|
||||
s[idx] *= 4
|
||||
return s
|
||||
|
||||
|
||||
def update_colorbar(im):
|
||||
#
|
||||
# This solves the problem of the colorbar being changed
|
||||
# when the forward and back buttons are used!!!
|
||||
#
|
||||
if hasattr(im, 'norecurse') and im.norecurse:
|
||||
im.norecurse=False
|
||||
return
|
||||
|
||||
im.norecurse=True
|
||||
# This is needed to update the colorbar
|
||||
# only change limits if array contains multiple values
|
||||
if np.min(im.get_array()) != np.max(im.get_array()):
|
||||
im.set_clim(vmin=np.min(im.get_array()),
|
||||
vmax=np.max(im.get_array()))
|
||||
|
||||
#
|
||||
# The meatiest part of this program, here we just go through and
|
||||
# set up the plot how we want it
|
||||
#
|
||||
|
||||
# First we set up the left-hand plot, which shows an overview map
|
||||
axes[0].set_title('Relative Displacement Map')
|
||||
|
||||
translations = translations.detach().cpu().numpy()
|
||||
|
||||
if convention.lower() != 'probe':
|
||||
translations = translations * -1
|
||||
|
||||
s = calculate_sizes(0)
|
||||
|
||||
nanomap_units_factor = get_units_factor(nanomap_units)
|
||||
nanomap = axes[0].scatter(nanomap_units_factor * translations[:,0],
|
||||
nanomap_units_factor * translations[:,1],
|
||||
s=s,c=values, picker=True)
|
||||
|
||||
axes[0].invert_xaxis()
|
||||
axes[0].set_facecolor('k')
|
||||
axes[0].set_xlabel('Translation x ('+nanomap_units+')', labelpad=1)
|
||||
axes[0].set_ylabel('Translation y ('+nanomap_units+')', labelpad=1)
|
||||
cb1 = plt.colorbar(nanomap, ax=axes[0], orientation='horizontal',
|
||||
format='%.2e',
|
||||
ticks=ticker.LinearLocator(numticks=5),
|
||||
pad=0.17,fraction=0.1)
|
||||
cb1.ax.set_title(nanomap_colorbar_title, size="medium", pad=5)
|
||||
cb1.ax.tick_params(labelrotation=20)
|
||||
if values is None:
|
||||
# This seems to do a good job of leaving the appropriate space
|
||||
# where the colorbar should have been to avoid stretching the
|
||||
# nanomap plot, while still not showing the (now useless) colorbar.
|
||||
cb1.remove()
|
||||
|
||||
# Now we set up the second plot, which shows the individual
|
||||
# diffraction patterns
|
||||
axes[1].set_title(image_title)
|
||||
#Plot in a basis if it exists, otherwise dont
|
||||
if basis is not None:
|
||||
axes[1].set_xlabel('X (' + image_units + ')')
|
||||
axes[1].set_ylabel('Y (' + image_units + ')')
|
||||
|
||||
example_im = get_image_func(0)
|
||||
if isinstance(example_im, t.Tensor):
|
||||
example_im = example_im.cpu().numpy()
|
||||
|
||||
if isinstance(basis,t.Tensor):
|
||||
np_basis = basis.detach().cpu().numpy()
|
||||
else:
|
||||
np_basis = basis
|
||||
# This fails if the basis is not rectangular
|
||||
basis_norm = np.linalg.norm(np_basis, axis = 0)
|
||||
basis_norm = basis_norm * get_units_factor(image_units)
|
||||
|
||||
extent = [0, example_im.shape[-1]*basis_norm[1], 0,
|
||||
example_im.shape[-2]*basis_norm[0]]
|
||||
else:
|
||||
axes[1].set_xlabel('j (pixels)')
|
||||
axes[1].set_ylabel('i (pixels)')
|
||||
extent=None
|
||||
|
||||
im=get_image_func(0)
|
||||
if len(im.shape) >= 3:
|
||||
im_idx=0
|
||||
axes[1].image_idx = im_idx
|
||||
im = im.reshape(-1,im.shape[-2],im.shape[-1])[im_idx]
|
||||
axes[1].text_box = axes[1].text(0.98, 0.98, str(im_idx), color='w',
|
||||
fontsize=14,
|
||||
horizontalalignment='right',
|
||||
verticalalignment='top',
|
||||
transform=axes[1].transAxes)
|
||||
axes[1].text_box.set_path_effects(
|
||||
[patheffects.Stroke(linewidth=2, foreground='black'),
|
||||
patheffects.Normal()])
|
||||
|
||||
meas = axes[1].imshow(im, extent=extent, cmap=cmap)
|
||||
|
||||
cb2 = plt.colorbar(meas, ax=axes[1], orientation='horizontal',
|
||||
format='%.2e',
|
||||
ticks=ticker.LinearLocator(numticks=5),
|
||||
pad=0.17,fraction=0.1)
|
||||
cb2.ax.tick_params(labelrotation=20)
|
||||
cb2.ax.set_title(image_colorbar_title, size="medium", pad=5)
|
||||
cb2.ax.callbacks.connect('xlim_changed', lambda ax: update_colorbar(meas))
|
||||
|
||||
# This function handles all the updating, except for moving the
|
||||
# slider value. This is done because the slider widget is
|
||||
# ultimately responsible for triggering an update, so all other
|
||||
# updates are done by changing the slider widget value which
|
||||
# then triggers this
|
||||
def update(idx, im_idx=None):
|
||||
# We have to explicitly make it an integer because the slider will
|
||||
# output floats (even if they are still integer-valued)
|
||||
idx = int(idx)
|
||||
|
||||
# Get the new data for this index
|
||||
im = get_image_func(idx)
|
||||
if len(im.shape) >= 3:
|
||||
if im_idx == None and hasattr(axes[1],'image_idx'):
|
||||
im_idx = axes[1].image_idx
|
||||
elif im_idx == None:
|
||||
im_idx=0
|
||||
axes[1].image_idx = im_idx
|
||||
axes[1].text_box.set_text(str(im_idx))
|
||||
im = im.reshape(-1,im.shape[-2],im.shape[-1])[im_idx]
|
||||
|
||||
# Now we resize the nanomap to show the new selection
|
||||
axes[0].collections[0].set_sizes(calculate_sizes(idx))
|
||||
|
||||
# And we update the data in the image as well
|
||||
|
||||
ax_im = axes[1].images[-1]
|
||||
ax_im.set_data(im)
|
||||
update_colorbar(ax_im)
|
||||
|
||||
|
||||
#
|
||||
# Now we define the functions to handle various kinds of events
|
||||
# that can be thrown our way
|
||||
#
|
||||
|
||||
# We start by creating the slider here, so it can be used
|
||||
# by the update hooks.
|
||||
slider = Slider(axslider, 'Image #', 0, translations.shape[0]-1, valstep=1, valfmt="%d")
|
||||
|
||||
# This handles scroll wheel and keypress events
|
||||
def on_action(event):
|
||||
im = get_image_func(0)
|
||||
if event.inaxes is axes[1] and len(im.shape) >=3:
|
||||
# This is triggered if the data to display has more than 2
|
||||
# dimensions (i.e. is an image stack) and the event originates
|
||||
# while the mouse is within the image display
|
||||
im = im.reshape(-1,im.shape[-2],im.shape[-1])
|
||||
im_idx = axes[1].image_idx
|
||||
|
||||
if event.key == 'up' or event.button == 'up' \
|
||||
or event.key == 'left':
|
||||
im_idx = (im_idx - 1) % im.shape[0]
|
||||
if event.key == 'down' or event.button == 'down' \
|
||||
or event.key == 'right':
|
||||
im_idx = (im_idx + 1) % im.shape[0]
|
||||
|
||||
axes[1].image_idx=im_idx
|
||||
slider.set_val(slider.val)#update(slider.val,im_idx=im_idx)
|
||||
return # This prevents the rest from also happening
|
||||
|
||||
# Otherwise the if statements can throw errors when the
|
||||
# event type isn't right, this way they just don't trigger
|
||||
if not hasattr(event, 'button'):
|
||||
event.button = None
|
||||
if not hasattr(event, 'key'):
|
||||
event.key = None
|
||||
|
||||
if event.key == 'up' or event.button == 'up' or event.key == 'left':
|
||||
idx = slider.val - 1
|
||||
elif event.key == 'down' or event.button == 'down' or event.key == 'right':
|
||||
idx = slider.val + 1
|
||||
else:
|
||||
# This prevents errors from being thrown on irrelevant key
|
||||
# or mouse input
|
||||
return
|
||||
|
||||
# Handle the wraparound and trigger the update
|
||||
idx = int(idx) % translations.shape[0]
|
||||
slider.set_val(idx)
|
||||
|
||||
# This handles "pick" events in the nanomap
|
||||
def on_pick(event):
|
||||
# If we don't filter on type of event, this will also capture,
|
||||
# for example, scroll events that happen over the nanomap
|
||||
if event.mouseevent.button == 1:
|
||||
slider.set_val(event.ind[0])
|
||||
|
||||
|
||||
# Here we connect the various update functions
|
||||
cid1 = fig.canvas.mpl_connect('pick_event',on_pick)
|
||||
cid2 = fig.canvas.mpl_connect('key_press_event',on_action)
|
||||
cid3 = fig.canvas.mpl_connect('scroll_event',on_action)
|
||||
# It's so dumb that matplotlib doesn't automatically track this for you
|
||||
fig.nanomap_cids = [cid1,cid2,cid3]
|
||||
slider.on_changed(update)
|
||||
|
||||
# Throw an extra update into the mix just to get rid of any things
|
||||
# (like the nanomap dot sizes) that otherwise would change on the
|
||||
# first update
|
||||
update(0)
|
||||
|
||||
return fig
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.polarization.polarization import *
|
||||
@@ -0,0 +1,217 @@
|
||||
import numpy as numpy
|
||||
import torch as t
|
||||
# Abe - again, we don't need math here. replace with torch-native functions
|
||||
# in all the definitions here. We have to use torch here if we want to be
|
||||
# able to calculate derivatives w.r.t. the polarizer angle, say.
|
||||
import math
|
||||
from math import sin
|
||||
from math import cos
|
||||
|
||||
__all__ = ['apply_linear_polarizer',
|
||||
'apply_phase_retardance',
|
||||
'apply_half_wave_plate',
|
||||
'apply_quarter_wave_plate',
|
||||
'apply_circular_polarizer',
|
||||
'apply_jones_matrix',
|
||||
'generate_linear_polarizer',
|
||||
'generate_birefringent_obj']
|
||||
|
||||
|
||||
def generate_linear_polarizer(pol_angle):
|
||||
single_angle = False
|
||||
|
||||
pol_angle = t.as_tensor(pol_angle).to(dtype=t.float32)
|
||||
if pol_angle.dim() == 0:
|
||||
pol_angle = t.unsqueeze(pol_angle,0)
|
||||
single_angle = True
|
||||
|
||||
pol_angle_rad = t.deg2rad(pol_angle)
|
||||
a = t.cos(pol_angle_rad) ** 2
|
||||
b = t.sin(pol_angle_rad) * t.cos(pol_angle_rad)
|
||||
c = b
|
||||
d = t.sin(pol_angle_rad) ** 2
|
||||
ab = t.stack((a, b), dim=-1)
|
||||
cd = t.stack((c, d), dim=-1)
|
||||
jones_matrices = t.stack((ab, cd), dim=-2)
|
||||
if single_angle:
|
||||
return jones_matrices[0].to(dtype=t.cfloat)
|
||||
else:
|
||||
return jones_matrices.to(dtype=t.cfloat)
|
||||
|
||||
|
||||
def apply_linear_polarizer(probe, polarizer, multiple_modes=True, transpose=True):
|
||||
"""
|
||||
Applies a linear polarizer to the probe
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (N)(P)x2xMxL tensor representing the probe, MxL - the size of the probe
|
||||
The angle between the fast-axis of the linear polarizer and the horizontal axis
|
||||
polarizer: t.Tensor
|
||||
A 1D tensor (N) representing the polarizer angles for each of the patterns (or a single tensor of shape (1))
|
||||
|
||||
Returns:
|
||||
--------
|
||||
linearly polarized probe: t.Tensor
|
||||
(N)(P)x2x1xMxL
|
||||
"""
|
||||
jones_matrices = generate_linear_polarizer(polarizer)
|
||||
return apply_jones_matrix(probe, jones_matrices, transpose=transpose, multiple_modes=multiple_modes)
|
||||
|
||||
|
||||
def apply_jones_matrix(probe, jones_matrix, transpose=True, multiple_modes=True):
|
||||
# print('probe', probe.shape, 'jones matrix', jones_matrix.shape)
|
||||
# if jones_matrix.shape == t.Size([5, 2, 2, 2, 2]):
|
||||
# print('probe', probe.shape, 'jones', jones_matrix.shape)
|
||||
# print('JONES', jones_matrix)
|
||||
"""
|
||||
Applies a given Jones matrix to the probe
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (N)(P)x2xMxL tensor representing the probe
|
||||
jones_matrix: t.tensor
|
||||
(N)x2x2x(M)x(L)
|
||||
|
||||
Returns:
|
||||
--------
|
||||
a probe with the jones matrix applied: t.Tensor
|
||||
(N)(P)x2xMxL
|
||||
|
||||
"""
|
||||
if transpose:
|
||||
if jones_matrix.dim() < 4:
|
||||
jones_matrix = jones_matrix[..., None, None]
|
||||
if multiple_modes:
|
||||
jones_matrix = jones_matrix.unsqueeze(-5)
|
||||
probe = probe[..., None, :, :]
|
||||
# if jones matrices do not differ from pattern to pattern
|
||||
if probe.dim() > jones_matrix.dim():
|
||||
jones_matrix = jones_matrix.unsqueeze(0)
|
||||
# vice versa
|
||||
elif jones_matrix.dim() > probe.dim():
|
||||
probe = probe.unsqueeze(0)
|
||||
# print('apply jonesmatrix: probe', probe.shape, 'matrix:', jones_matrix)
|
||||
jones_matrix = jones_matrix.transpose(-1, -3).transpose(-2, -4)
|
||||
probe = probe.transpose(-1, -3).transpose(-2, -4)
|
||||
output = t.matmul(jones_matrix, probe).transpose(-2, -4).transpose(-1, -3).squeeze(-3)
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def apply_phase_retardance(probe, phase_shift, multiple_modes=True):
|
||||
"""
|
||||
Shifts the y-component of the field wrt the x-component by a given phase shift
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (...)x2x1xMxL tensor representing the probe
|
||||
phase_shift: float
|
||||
phase shift in degrees
|
||||
|
||||
Returns:
|
||||
--------
|
||||
probe: t.Tensor
|
||||
(...)x2x1xMxL
|
||||
"""
|
||||
theta = t.as_tensor(phase_shift, dtype=t.float32)
|
||||
theta = t.deg2rad(theta)
|
||||
probe = probe.to(dtype=t.cfloat)
|
||||
jones_matrix = t.tensor([[1, 0], [0, t.exp(phase_shift)]]).to(dtype=t.cfloat)
|
||||
polarized = apply_jones_matrix(probe, jones_matrix, multiple_modes=multiple_modes)
|
||||
|
||||
return polarized
|
||||
|
||||
def apply_circular_polarizer(probe, left_polarized=True, multiple_modes=True):
|
||||
"""
|
||||
Applies a circular polarizer to the probe
|
||||
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (...)x2xMxL tensor representing the probe
|
||||
left_polarizd: bool
|
||||
True for the left-polarization, False for the right
|
||||
|
||||
Returns:
|
||||
--------
|
||||
circularly polarized probe: t.Tensor
|
||||
(...)x2xMxL
|
||||
"""
|
||||
probe = probe.to(dtype=t.cfloat)
|
||||
if left_polarized:
|
||||
jones_matrix = (1/2 * t.tensor([[1, -1j], [1j, 1]])).to(dtype=t.cfloat)
|
||||
else:
|
||||
jones_matrix = 1/2 * t.tensor([[1, 1j], [-1j, 1]]).to(dtype=t.cfloat)
|
||||
polarized = apply_jones_matrix(probe, jones_matrix, multiple_modes=multiple_modes)
|
||||
return polarized
|
||||
|
||||
def apply_quarter_wave_plate(probe, fast_axis_angle, multiple_modes=True):
|
||||
"""
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (...)x2x1xMxL tensor representing the probe, MxL - the size of the probe
|
||||
fast_axis_angle: float
|
||||
The angle between the fast-axis of the polarizer and the horizontal axis
|
||||
|
||||
Returns:
|
||||
--------
|
||||
polarized probe: t.Tensor
|
||||
(...)x2x1xMxL
|
||||
"""
|
||||
probe = probe.to(dtype=t.cfloat)
|
||||
theta = math.radians(fast_axis_angle)
|
||||
exponent = t.exp(-1j * math.pi / 4 * t.ones(2, 2))
|
||||
jones_matrix = exponent* t.tensor([[(cos(theta))**2 + 1j * (sin(theta))**2, (1 - 1j) * sin(theta) * cos(theta)], [(1 - 1j) * sin(theta) * cos(theta), (sin(theta))**2 + 1j * (cos(theta))**2]]).to(dtype=t.cfloat)
|
||||
out = apply_jones_matrix(probe, jones_matrix, multiple_modes=multiple_modes)
|
||||
|
||||
return out
|
||||
|
||||
def apply_half_wave_plate(probe, fast_axis_angle, multiple_modes=True):
|
||||
"""
|
||||
Parameters:
|
||||
----------
|
||||
probe: t.Tensor
|
||||
A (...)x2x1xMxL tensor representing the probe, MxL - the size of the probe
|
||||
fast_axis_angle: float
|
||||
The angle between the fast-axis of the polarizer and the horizontal axis
|
||||
|
||||
Returns:
|
||||
--------
|
||||
polarized probe: t.Tensor
|
||||
(...)x2x1xMxL
|
||||
"""
|
||||
probe = probe.to(dtype=t.cfloat)
|
||||
theta = math.radians(fast_axis_angle)
|
||||
exponent = t.exp(-1j * math.pi / 2 * t.ones(2, 2))
|
||||
jones_matrix = exponent * t.tensor([[(cos(theta))**2 - (sin(theta))**2, 2 * sin(theta) * cos(theta)], [2 * sin(theta) * cos(theta), (sin(theta))**2 - (cos(theta))**2]]).to(dtype=t.cfloat)
|
||||
out = apply_jones_matrix(probe, jones_matrix, multiple_modes=multiple_modes)
|
||||
|
||||
return out
|
||||
|
||||
def generate_birefringent_obj(fast_axis=90, phase_ret=10, atten_fast=1, atten_ret=1, global_phase=0):
|
||||
def to_rad(angle):
|
||||
angle = t.as_tensor(angle, dtype=t.float32)
|
||||
angle = t.deg2rad(angle)
|
||||
return angle
|
||||
|
||||
fast_axis = to_rad(fast_axis)
|
||||
phase_ret = to_rad(phase_ret)
|
||||
global_phase = to_rad(global_phase)
|
||||
|
||||
def coord_rot(angle):
|
||||
a = t.stack((t.cos(angle), t.sin(angle)), dim=-1)
|
||||
b = t.stack((-t.sin(angle), t.cos(angle)), dim=-1)
|
||||
return t.stack((a, b), dim=-2).to(dtype=t.cfloat)
|
||||
|
||||
r1 = coord_rot(-fast_axis)
|
||||
r2 = coord_rot(fast_axis)
|
||||
p = t.exp(global_phase * 1j) * t.as_tensor([[atten_fast, 0], [0, atten_ret * t.exp(phase_ret*1j)]], dtype=t.cfloat)
|
||||
return t.matmul(r1, t.matmul(p, r2))
|
||||
@@ -0,0 +1 @@
|
||||
from CDTools.tools.propagators.propagators import *
|
||||
@@ -0,0 +1,646 @@
|
||||
"""This module contains various propagators for light fields
|
||||
|
||||
All the functions here are designed for use in an automatic differentiation
|
||||
ptychography model. Each function implements a different propagator.
|
||||
"""
|
||||
|
||||
import torch as t
|
||||
from torch.nn.functional import grid_sample
|
||||
from scipy import fftpack
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
__all__ = ['far_field', 'near_field',
|
||||
'generate_angular_spectrum_propagator',
|
||||
'inverse_far_field', 'inverse_near_field',
|
||||
'generate_high_NA_k_intensity_map',
|
||||
'high_NA_far_field',
|
||||
'generate_generalized_angular_spectrum_propagator']
|
||||
|
||||
def far_field(wavefront):
|
||||
"""Implements a far-field propagator in torch
|
||||
|
||||
This accepts a torch tensor, where the last dimension
|
||||
represents the real and imaginary components of the wavefield,
|
||||
and returns the far-field propagated version of it assuming it matches the
|
||||
detector dimensions. It assumes that the
|
||||
propagation is purely far-field, without checking that the geometry
|
||||
is consistent with that assumption.
|
||||
|
||||
|
||||
It also assumes that the real space wavefront is stored in an array
|
||||
[i,j] where i corresponds to the y-axis and j corresponds to the
|
||||
x-axis, with the origin following the CS standard of being in the
|
||||
upper right. The zero frequency component of the propagated wavefield is
|
||||
shifted to the center of the array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefront : torch.Tensor
|
||||
The JxNxMx2 stack of complex wavefronts to be propagated
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagated : torch.Tensor
|
||||
The JxNxMx2 propagated wavefield
|
||||
"""
|
||||
|
||||
shifted = t.fft.ifftshift(wavefront, dim=(-1,-2))
|
||||
propagated = t.fft.fft2(shifted, norm='ortho')
|
||||
return t.fft.fftshift(propagated, dim=(-1,-2))
|
||||
|
||||
|
||||
def inverse_far_field(wavefront):
|
||||
"""Implements the inverse of the far-field propagator in torch
|
||||
|
||||
This accepts a torch tensor, where the last dimension
|
||||
represents the real and imaginary components of the propagated wavefield,
|
||||
and returns the un-propagated array.
|
||||
|
||||
It assumes that the real space wavefront is stored in an array
|
||||
[i,j] where i corresponds to the y-axis and j corresponds to the
|
||||
x-axis, with the origin following the CS standard of being in the
|
||||
upper right. The zero frequency component of the propagated wavefield is
|
||||
assumed to be the center of the array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefront : torch.Tensor
|
||||
The JxNxMx2 stack of complex wavefronts propagated to the far-field
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagated : torch.Tensor
|
||||
The JxNxMx2 exit wavefield
|
||||
"""
|
||||
shifted = t.fft.ifftshift(wavefront, dim=(-1,-2))
|
||||
propagated = t.fft.ifft2(shifted, norm='ortho')
|
||||
return t.fft.fftshift(propagated, dim=(-1,-2))
|
||||
|
||||
|
||||
def generate_high_NA_k_intensity_map(sample_basis, det_basis,det_shape,distance, wavelength, *args, lens=False, **kwargs):
|
||||
"""Generates k-space and intensity maps to allow for high-NA far-field propagation of light
|
||||
|
||||
At high numerical apertures or for very tilted samples, the simple
|
||||
linear map between location on the detector and location in k-space
|
||||
starts to break down. In addition, at angles above roughly 15 degrees,
|
||||
a correction is needed to account for the decreasing solid angle
|
||||
intersected by each pixel on the detector.
|
||||
|
||||
This function generates a map which can be used to apply both corrections
|
||||
via the high_NA_far_field propagator. The k-map which is output is
|
||||
defined as a map between pixel location on the detector and locations
|
||||
in the output of the standard, low-NA far-field propagated wavefield.
|
||||
The output coordinate system is defined to run from -1 to 1 in both
|
||||
directions - this allows for compatibility with pytorch's grid_sample
|
||||
function. Some detector pixels may be mapped to values outside the
|
||||
rangel [-1,1], depending on the respective sample and detector geometries.
|
||||
This is most likely to be the case if the major correction is due to a
|
||||
tilted sample.
|
||||
|
||||
The intensity map is simply an object, the shape of the detector, which
|
||||
encodes intensity corrections between 0 and 1 per pixel.
|
||||
|
||||
If the optional "lens" parameter is set to True, the intensity map will
|
||||
be set to a uniform map, and the distortion of Fourier space due to the
|
||||
flat nature of the detector (that is, the portion of the distortion
|
||||
that exists even if the sample is not tilted) will be disabled. This is
|
||||
to account for the fact that a good, infinity-conjugate imaging lens
|
||||
will do it's best to correct for these abberations in the lens. Of course,
|
||||
the lens will not be perfect, but in such a case it is a better
|
||||
approximation to assume that the lens is perfect than to assume that it
|
||||
is not there at all.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sample_basis: array
|
||||
The 3x2 sample basis, in real space
|
||||
det_basis: array
|
||||
The 3x2 detector basis, in real space
|
||||
det_shape: array
|
||||
The length-2 shape of the detector array, (N,M)
|
||||
distance: float
|
||||
The sample-to-detector distance
|
||||
wavelength: float
|
||||
The wavelength of light being propagated
|
||||
lens: bool
|
||||
Whether the diffraction pattern is formed by a lens or not.
|
||||
|
||||
Returns
|
||||
-------
|
||||
k_map : torch.Tensor
|
||||
An NxMx2 tensor mapping detector pixels to locations in the low-NA propagated wavefield
|
||||
intensity_map : torch.Tensor
|
||||
An NxM tensor encoding the high-NA intensity correction
|
||||
"""
|
||||
|
||||
# First we convert things to numpy
|
||||
try:
|
||||
sample_basis = sample_basis.detach().cpu().numpy()
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
det_basis = det_basis.detach().cpu().numpy()
|
||||
except:
|
||||
pass
|
||||
|
||||
det_shape = np.array(tuple(det_shape))
|
||||
|
||||
try:
|
||||
distance = distance.detach().cpu().numpy()[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
wavelength = wavelength.detach().cpu().numpy()[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
# The next order of business is to calculate the k values associated with
|
||||
# each pixel.
|
||||
|
||||
i_arr = np.arange(det_shape[0])
|
||||
i_arr = i_arr - np.mean(i_arr)
|
||||
j_arr = np.arange(det_shape[1])
|
||||
j_arr = j_arr - np.mean(j_arr)
|
||||
Is, Js = np.meshgrid(i_arr,j_arr,indexing='ij')
|
||||
samp_det_vec = np.cross(det_basis[:,0],det_basis[:,1])
|
||||
samp_det_vec *= distance / np.linalg.norm(samp_det_vec)
|
||||
|
||||
# This could potentially correct for a mistake in the implied
|
||||
# propagation direction (e.g. choosing e^ikx instead of e^-ikx)
|
||||
# This appears to be correct, based on empirical evidence from
|
||||
# a grazing incidence reflection experiment at 10 degrees
|
||||
# on the optical table
|
||||
samp_det_vec *= -1
|
||||
|
||||
if lens == False:
|
||||
# This correctly reproduces the sample-to-each-pixel vectors
|
||||
# in the case where the diffraction pattern is actually formed
|
||||
# by Fraunhoffer diffraction
|
||||
Rs = np.tensordot(det_basis,np.stack([Is,Js]),axes=1) \
|
||||
+ samp_det_vec[:,None,None]
|
||||
else:
|
||||
# This forms a distorted set of vectors designed to produce the
|
||||
# correct Fourier space map in the case where an imaging lens is
|
||||
# used in the 2f geometry. One should not read too much meaning
|
||||
# into these vectors, they are simply set up to produce the
|
||||
# correct final K-map
|
||||
Rs = np.tensordot(det_basis,np.stack([Is,Js]),axes=1)#
|
||||
Rs += (samp_det_vec / np.linalg.norm(samp_det_vec))[:,None,None] * \
|
||||
np.sqrt(np.sum((samp_det_vec)**2)-np.sum(Rs**2,axis=0))[None,:,:]
|
||||
|
||||
k0 = 2*np.pi/wavelength
|
||||
|
||||
Ks = k0 * Rs / np.linalg.norm(Rs, axis=0)
|
||||
|
||||
# My attempt at seeing what happens if I flip the Ks
|
||||
#Ks *= -1
|
||||
|
||||
# This is the cosine of the angle with the detector normal
|
||||
intensity_map = np.tensordot(samp_det_vec/(k0*distance),Ks,axes=1)
|
||||
if lens:
|
||||
# Set the intensity map to be uniform if a lens is being used
|
||||
intensity_map = np.ones_like(intensity_map)
|
||||
|
||||
intensity_map = t.Tensor(intensity_map).to(*args, **kwargs)
|
||||
|
||||
|
||||
# This accounts for the implied phase ramp along the exit wave direction
|
||||
# In other words, it prevents the diffraction pattern from sliding off the
|
||||
# detector when the sample is tilted but represented by an object with
|
||||
# uniform phase.
|
||||
Ks -= k0 * samp_det_vec[:,None,None] / distance
|
||||
|
||||
# A potential alternative when Ks are flipped
|
||||
#Ks += k0 * samp_det_vec[:,None,None] / distance
|
||||
|
||||
|
||||
# Now we move on to finding the conversion into k-space
|
||||
# for the sample grid. It turns out we can do this by multiplying
|
||||
# them with the real space basis (dual of the reciprocal space
|
||||
# basis is the real space basis). In fact, because we want to return
|
||||
# values scaled to the overall size of the k-space window, we don't
|
||||
# even need the shape of the sample array
|
||||
|
||||
k_map = np.tensordot(2*sample_basis.transpose()[::-1,:] / (2*np.pi),Ks,axes=1)
|
||||
k_map = t.Tensor(np.moveaxis(k_map,0,2)).to(*args, **kwargs)
|
||||
|
||||
# Potentially we need a correction to account for the discrete nature
|
||||
# of the FFT
|
||||
|
||||
# And finally, we need to convert the results to pytorch
|
||||
|
||||
return k_map, intensity_map
|
||||
|
||||
|
||||
|
||||
|
||||
def high_NA_far_field(wavefront, k_map, intensity_map=None):
|
||||
"""Performs a far-field propagation step including a correction for high-NA scenarios
|
||||
|
||||
Two major corrections need to be performed when propagating light fields
|
||||
into the far field at high numerical aperture or when the sample is
|
||||
tilted as compared to the detector. The first correction is a deviation
|
||||
from the linear relationship between detector position and spatial
|
||||
frequency in the near field. This is accounted for with the k_map
|
||||
argument, as generated by the generate_high_NA_k_intensity_map
|
||||
function.
|
||||
|
||||
The second correction is the change in the solid angle which each pixel
|
||||
subtends at high NA. This is accounted for with an optional intensity
|
||||
map. This is kept optional because some detectors - specifically, those
|
||||
for penetrating radiation - may either not need a correction or need
|
||||
a different correction due to the volumetric nature of the pixels.
|
||||
|
||||
If the k-map maps any pixels on the detector to pixels outside of the
|
||||
k-space range of the wavefront, these will be set to zero. This is in
|
||||
keeping with the typical assumption that the sample is band-limited to
|
||||
the Nyquist frequency for the array on which it is sampled.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefront : torch.Tensor
|
||||
The JxNxMx2 stack of complex wavefronts propagated to the far-field
|
||||
k_map : torch.Tensor
|
||||
The NxMx2 map accounting for high NA distortion, as generated by generate_high_NA_k_intensity_map
|
||||
intensity_map : torch.Tensor
|
||||
The optional NxM tensor accounting for the intensity variation across the detector
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagated : torch.Tensor
|
||||
The JxNxMx2 exit wavefield
|
||||
|
||||
|
||||
"""
|
||||
low_NA_wavefield = far_field(wavefront)
|
||||
# I'm going to need to separately interpolate the real and complex parts
|
||||
# This can be done
|
||||
|
||||
k_map = k_map[None,:,:,:]
|
||||
# Will only work for a 4D wavefile stack.
|
||||
#plt.figure()
|
||||
#plt.pcolormesh(k_map[0,:,:,0].cpu().numpy(),k_map[0,:,:,1].cpu().numpy(),
|
||||
# np.ones_like(k_map[0,:-1,:-1,0].cpu().numpy()))
|
||||
#plt.show()
|
||||
def process_wavefield_stack(low_NA_wavefield):
|
||||
# grid_sample doesn't work on complex-valued wavefields
|
||||
real_output = grid_sample(low_NA_wavefield[None,:,:,:].real,k_map,mode='bilinear',padding_mode='zeros', align_corners=False)
|
||||
imag_output = grid_sample(low_NA_wavefield[None,:,:,:].imag,k_map,mode='bilinear',padding_mode='zeros', align_corners=False)
|
||||
|
||||
result = real_output[0,:,:,:] + 1j * imag_output[0,:,:,:]
|
||||
|
||||
if intensity_map is not None:
|
||||
result = result * intensity_map[None,:,:]
|
||||
|
||||
return result
|
||||
|
||||
original_dim = wavefront.dim()
|
||||
if original_dim == 2:
|
||||
result = process_wavefield_stack(low_NA_wavefield[None,:,:])
|
||||
return result[0,:,:]
|
||||
if original_dim == 3:
|
||||
result = process_wavefield_stack(low_NA_wavefield)
|
||||
return result
|
||||
if original_dim == 4:
|
||||
result = []
|
||||
for i in range(low_NA_wavefield.size()[0]):
|
||||
result.append(process_wavefield_stack(low_NA_wavefield[i,:,:,:]))
|
||||
return t.stack(result)
|
||||
else:
|
||||
raise IndexError('Wavefield had incorrect number of dimensions')
|
||||
|
||||
|
||||
|
||||
def generate_angular_spectrum_propagator(shape, spacing, wavelength, z, *args, remove_z_phase=False, bandlimit=None, **kwargs):
|
||||
"""Generates an angular-spectrum based near-field propagator from experimental quantities
|
||||
|
||||
This function generates an angular-spectrum based near field
|
||||
propagator that will work on torch Tensors. The function is structured
|
||||
this way - to generate the propagator first - because the
|
||||
generation of the propagation mask is a bit expensive and if this
|
||||
propagator is used in a reconstruction program, then it will be best
|
||||
to calculate this mask once and reuse it.
|
||||
|
||||
Formally, this propagator is the the fourier transform of the convolution
|
||||
kernel for light propagation in free space
|
||||
|
||||
If the optional bandlimit parameter is set, the propagator will be set
|
||||
to zero beyond an explicit bandlimiting frequency. This is helpful if the
|
||||
propagator will be used in a repeated multiply/propagate framework such
|
||||
as a multislice algorithm, where it helps to prevent aliasing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : array
|
||||
The shape of the arrays to be propagated
|
||||
spacing : array
|
||||
The pixel size in each dimension of the arrays to be propagated
|
||||
wavelength : float
|
||||
The wavelength of light to simulate propagation of
|
||||
z : float
|
||||
The distance to simulate propagation over
|
||||
remove_z_phase : bool
|
||||
Default False, whether to remove the dominant z-direction phase dependence
|
||||
bandlimit : float
|
||||
Optional, a fraction of the full detector radius beyond which to set the propagator to zero.
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagator : torch.Tensor
|
||||
A phase mask which accounts for the phase change that each plane wave will undergo.
|
||||
"""
|
||||
|
||||
# Internally, the generalized propagation function is used, so we start
|
||||
# by creating an appropriate basis
|
||||
# This creates a real-valued tensor which matches the kind of complex
|
||||
# number dtype requested in **kwargs
|
||||
if 'dtype' in kwargs:
|
||||
basis = t.real(t.zeros([3,2], **kwargs))
|
||||
else:
|
||||
basis = t.zeros([3,2], dtype=t.float32)
|
||||
spacing = t.as_tensor(spacing, dtype=basis.dtype)
|
||||
|
||||
basis[1,0] = -spacing[0]
|
||||
basis[0,1] = -spacing[1]
|
||||
# And similarly, the offset is just z along the z direction
|
||||
#offset = t.tensor([0,0,z], dtype=basis.dtype)
|
||||
offset = t.zeros(3, dtype=basis.dtype)
|
||||
offset[2] = z
|
||||
|
||||
# And we call the generalized function!
|
||||
propagator = generate_generalized_angular_spectrum_propagator(shape, basis,
|
||||
wavelength, offset,
|
||||
propagate_along_offset=remove_z_phase, **kwargs)
|
||||
if z < 0:
|
||||
propagator = t.conj(propagator)
|
||||
|
||||
|
||||
# Bandlimiting is not implemented in the generalized function, because it
|
||||
# has a less clear meaning in that setting, so we apply it here instead
|
||||
if bandlimit is not None:
|
||||
# No need to multiply by 2pi
|
||||
ki = 2 * np.pi * t.fft.fftfreq(shape[0],spacing[0])
|
||||
kj = 2 * np.pi * t.fft.fftfreq(shape[1],spacing[1])
|
||||
Ki, Kj = t.meshgrid(ki,kj, indexing='ij')
|
||||
min_radius = min(t.max(ki),t.max(kj))
|
||||
Rs = t.sqrt((Ki/t.max(ki))**2 + (Kj/t.max(kj))**2)
|
||||
propagator = propagator * (Rs < bandlimit)
|
||||
|
||||
return propagator
|
||||
|
||||
|
||||
|
||||
def generate_generalized_angular_spectrum_propagator(shape, basis, wavelength, offset_vector, *args, propagation_vector=None, propagate_along_offset=False, **kwargs):
|
||||
"""Generates an angular-spectrum based near-field propagator from experimental quantities
|
||||
|
||||
This function generates an angular-spectrum based near field
|
||||
propagator that will work on torch Tensors. The function is structured
|
||||
this way - to generate the propagator first - because the
|
||||
generation of the propagation mask is a bit expensive and if this
|
||||
propagator is used in a reconstruction program, it will be best
|
||||
to calculate this mask once and then reuse it it.
|
||||
|
||||
Formally, this propagator is the fourier transform of the convolution
|
||||
kernel for light propagation in free space. It will map a light field
|
||||
at an input plane, with the size and shape defined by the shape and basis
|
||||
inputs, and map it to a plane of the same size and shape offset by the
|
||||
offset vector. It is designed to work on any wavefield defined on an
|
||||
array of parallelograms.
|
||||
|
||||
|
||||
In addition, if the propagation_vector is set, there is an assumed
|
||||
phase ramp applied to the wavefield before propagation, defined such
|
||||
that a feature with uniform phase will propagate along the direction of
|
||||
the propagation vector. This will also remove the phase variation along
|
||||
the propagation direction, because it makes the most physical sense to
|
||||
regard this choice as removing the dominant phase variation in 3D, allowing
|
||||
for the generation of a smoothly varying wavefield over 3D volumes.
|
||||
This decision provides the best numerical stability and allows for the
|
||||
simple setup of light fields copropagating with the coordinate system.
|
||||
|
||||
If the propagate_along_offset option is set to True, then the propagation
|
||||
vector will be set equal to the offset vector. This overrides the
|
||||
propagation_vector option
|
||||
|
||||
Note that, unlike in the case of the simple angular spectrum propagator,
|
||||
the direction of "forward propagation" is defined by the propagation
|
||||
vector, or (if the propagation vector is not defined), the offset
|
||||
vector. Therefore, in the simple case of a perpendicular offset,
|
||||
there will be no difference between using an offset vector or the
|
||||
negative of the offset vector. This is because, for the light
|
||||
propagation problem to be well posed, the assumption must be made that
|
||||
light only passes through the plane of the known wavefield in one
|
||||
direction. We always assume that light passes through the initial plane
|
||||
travelling in the direction of the final plane.
|
||||
|
||||
Practically, if one wants to simulate inverse propagation, there are
|
||||
two possible approaches. First, one can use the inverse_near_field
|
||||
function, which simulates the inverse propagation problem and therefore
|
||||
will naturally simulate propagation in the opposite direction. Second,
|
||||
one can explicitly include a propagation_vector argument, in the direction
|
||||
opposite to the offset vector. Note for both cases that this function
|
||||
will only return propagators which suppress evanescent waves. Thus,
|
||||
propagating forward and then backward by either of these two methods
|
||||
will lead to a supression of evanescant waves. If you need a propagator
|
||||
that will cause evanescent waves to undergo exponential growth, good
|
||||
for you, but this function will not provide it for you.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : array
|
||||
The shape of the arrays to be propagated
|
||||
basis : array
|
||||
The (2x3) set of basis vectors describing the array to be propagated
|
||||
wavelength : float
|
||||
The wavelength of light to simulate propagation of
|
||||
offset_vector : array
|
||||
The displacement to propagate the wavefield along.
|
||||
propagation_vector : array
|
||||
The vector along which to include an implied phase ramp to propagate uniform phase features along, if set
|
||||
propagate_along_offset : bool
|
||||
Overrides propagation_vector, sets the propagation vector to equal the offset vector if set.
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagator : torch.Tensor
|
||||
A phase mask which accounts for the phase change that each plane wave will undergo.
|
||||
"""
|
||||
|
||||
# make sure everything is in pytorch, and set the propagation vector
|
||||
# appropriately if propagate_along_offset is chosen
|
||||
basis = t.as_tensor(basis)
|
||||
offset_vector = t.as_tensor(offset_vector, dtype=basis.dtype)
|
||||
|
||||
if propagate_along_offset:
|
||||
propagation_vector = offset_vector
|
||||
|
||||
if propagation_vector is not None:
|
||||
propagation_vector = t.as_tensor(propagation_vector, dtype=basis.dtype)
|
||||
|
||||
#
|
||||
# In this section, we calculate the wavevectors associated with each
|
||||
# pixel in Fourier space. This is the meat of the function
|
||||
#
|
||||
|
||||
# First we calculate a dual basis for the real space grid
|
||||
inv_basis = t.linalg.pinv(basis).transpose(0,1)
|
||||
|
||||
# Then we calculate the frequencies in (i,j) space
|
||||
ki = 2 * np.pi * t.fft.fftfreq(shape[0], dtype=inv_basis.dtype)
|
||||
kj = 2 * np.pi * t.fft.fftfreq(shape[1], dtype=inv_basis.dtype)
|
||||
K_ij = t.stack(t.meshgrid(ki,kj, indexing='ij'))
|
||||
|
||||
# Now we convert these to frequencies in reciprocal space
|
||||
# These frequencies span the 2D plane of the input wavefield,
|
||||
# hence K_ip for "in-plane"
|
||||
K_ip = t.tensordot(inv_basis, K_ij, dims=1)
|
||||
|
||||
|
||||
# Now, we need to generate the out-of-plane direction, so we can
|
||||
# expand these Ks to the full Ks in 3D reciprocal space.
|
||||
|
||||
# This is broken down into 2 steps to avoid floating point underflow
|
||||
# which was a real problem that showed up for electron ptycho
|
||||
b1_dir = basis[:,0] / t.linalg.norm(basis[:,0])
|
||||
b2_dir = basis[:,1] / t.linalg.norm(basis[:,1])
|
||||
perpendicular_dir = t.cross(b1_dir,b2_dir)
|
||||
# Note that we cannot use in-place operations if we want to be able to
|
||||
# use automatic differentiation successfully
|
||||
perpendicular_dir = perpendicular_dir / t.linalg.norm(perpendicular_dir)
|
||||
|
||||
# We set the sign of the propagation direction appropriately
|
||||
if propagation_vector is not None:
|
||||
perpendicular_dir = perpendicular_dir \
|
||||
* t.sign(t.dot(perpendicular_dir,propagation_vector))
|
||||
else:
|
||||
pass
|
||||
perpendicular_dir = perpendicular_dir * \
|
||||
t.sign(t.dot(perpendicular_dir,offset_vector))
|
||||
|
||||
# Then, if we have a propagation vector, we shift the in-plane
|
||||
# components of all the pixels to be centered around the in-plane
|
||||
# component of the propagation vector.
|
||||
if propagation_vector is not None:
|
||||
prop_dir = (propagation_vector /
|
||||
t.linalg.norm(propagation_vector))
|
||||
K_0 = 2*np.pi / wavelength * prop_dir
|
||||
K_0_ip = K_0 - t.dot(perpendicular_dir,K_0) * perpendicular_dir
|
||||
K_ip += K_0_ip[:,None,None]
|
||||
else:
|
||||
K_0 = t.zeros_like(offset_vector)
|
||||
|
||||
# Now, we have accurate in-plane values for K, so we can calculate the
|
||||
# out-of-plane part. We start by calculating it's squared magnitude
|
||||
K_oop_squared = (2*np.pi/wavelength)**2 - t.linalg.norm(K_ip,dim=0)**2
|
||||
|
||||
# Then, we take the square root and assign it the appropriate direction,
|
||||
# adding to get the full 3D wavevectors. Note that we convert to complex
|
||||
# before the square root to appropriately map negative numbers to
|
||||
# complex frequencies
|
||||
K = K_ip + perpendicular_dir[:,None,None] \
|
||||
* t.sqrt(t.complex(K_oop_squared,t.zeros_like(K_oop_squared)))
|
||||
|
||||
#
|
||||
# In this section, we take the inner product of the calcualted
|
||||
# wavevectors with the offset vector, to get the phase shift
|
||||
# experienced by each plane wave.
|
||||
#
|
||||
|
||||
# We need to convert to complex because K is complex
|
||||
offset_vector = t.complex(offset_vector,t.zeros_like(offset_vector))
|
||||
|
||||
# We then subtract off K_0, essentially setting the phase offset
|
||||
# experienced by K_0 to 0. K_0 will already have been set to 0 if
|
||||
# there was no propagation vector set.
|
||||
K_m_K_0 = K - K_0[:,None,None]
|
||||
# We actually calculate the phase mask
|
||||
phase_mask = t.tensordot(offset_vector,K_m_K_0, dims=1)
|
||||
|
||||
# And we take the conjugate if needed, which makes sure that the
|
||||
# imaginary part is always positive (supressing evanescent waves)
|
||||
if propagation_vector is not None \
|
||||
and t.sign(t.dot(propagation_vector,offset_vector.real)) == -1:
|
||||
phase_mask = t.conj(phase_mask)
|
||||
|
||||
# We return the final mask, in the form asked for!
|
||||
return t.exp(1j*phase_mask).to(*args,**kwargs)
|
||||
|
||||
|
||||
def near_field(wavefront, angular_spectrum_propagator):
|
||||
""" Propagates a wavefront via the angular spectrum method
|
||||
|
||||
This function accepts an 3D torch tensor, where the last dimension
|
||||
represents the real and imaginary components of the wavefield, and
|
||||
returns the near-field propagated version of it. It does this
|
||||
using the supplied angular spectrum propagator, which is a premade
|
||||
phase mask representing the Fourier transform of the kernel for
|
||||
light propagation in the desired geometry.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefront : torch.Tensor
|
||||
The (Leading Dims)xNxM stack of complex wavefronts to be propagated
|
||||
angular_spectrum_propagator : torch.Tensor
|
||||
The NxM phase mask to be applied during propagation
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagated : torch.Tensor
|
||||
The propagated wavefront
|
||||
"""
|
||||
return t.fft.ifft2(angular_spectrum_propagator * t.fft.fft2(wavefront))
|
||||
|
||||
|
||||
|
||||
def inverse_near_field(wavefront, angular_spectrum_propagator):
|
||||
""" Inverse propagates a wavefront via the angular spectrum method
|
||||
|
||||
This function accepts an 3D torch tensor, where the last dimension
|
||||
represents the real and imaginary components of the wavefield, and
|
||||
returns the near-field propagated version of it. It does this
|
||||
using the supplied angular spectrum propagator, which is a premade
|
||||
phase mask.
|
||||
|
||||
It propagates the wave using the complex conjugate of the supplied
|
||||
phase mask. This corresponds to propagation backward across the original
|
||||
propagation region - however, the treatment of evanescent waves is such
|
||||
that evanescent waves will decay both during the forward propagation and
|
||||
inverse propagation. This is done for reasons of numerical stability,
|
||||
as the choice to magnify evanescent waves during the inverse propagation
|
||||
process will quickly lead to magnification of any small amount of noise at
|
||||
frequencies larger than k_0, and in most typical situations will even
|
||||
lead to overflow of the floating point range. If evanescent waves need
|
||||
to be treated appropriately for any reason, it is recommended to use the
|
||||
"magnify_evanescent" option in the appropriate helper function used to
|
||||
generate the propagation phase mask. In this case, evanescent waves will
|
||||
be magnified both when used with the forward and inverse near field
|
||||
functions
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
wavefront : torch.Tensor
|
||||
The JxNxMx2 stack of complex wavefronts to be propagated
|
||||
angular_spectrum_propagator : torch.Tensor
|
||||
The NxM phase mask to be applied during propagation
|
||||
|
||||
Returns
|
||||
-------
|
||||
propagated : torch.Tensor
|
||||
The inverse propagated wavefront
|
||||
"""
|
||||
return t.fft.ifft2(t.fft.fft2(wavefront)
|
||||
* t.conj(angular_spectrum_propagator))
|
||||
|
||||
|
||||
|
||||
# I think it would be worthwhile to implement an FFT-DI based strategy as
|
||||
# well, especially for probe initialization where the propagation distance
|
||||
# can be large relative to what the angular spectrum method can reliably handle
|
||||
|
||||
|
||||
Reference in New Issue
Block a user