Skip to content

Commit 597e5b1

Browse files
authored
Merge pull request #709 from bsavitzky/dev14
get_vacuum_probe bugfix
2 parents a68396b + 3a2d794 commit 597e5b1

File tree

4 files changed

+150
-48
lines changed

4 files changed

+150
-48
lines changed

py4DSTEM/braggvectors/probe.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,9 @@ def zero_vacuum(
307307
alpha_max=1.2,
308308
):
309309
"""
310-
Sets pixels outside of the probe's central disk to zero.
310+
Sets pixels outside of the probe's central disk to zero using a
311+
hard mask at alpha*alpha_max. To use a continuous mask, try
312+
zero_vacuum_sinusoid.
311313
312314
The probe origin and convergence semiangle must be set for this
313315
method to run - these can be set using `measure_disk`. Pixels are
@@ -335,6 +337,57 @@ def zero_vacuum(
335337
self.probe *= mask
336338
pass
337339

340+
def zero_vacuum_sinusoid(self, s=0.1, w=1, p=1, update_inplace=True):
341+
"""
342+
Sets pixels outside of the probe's central disk to zero using a
343+
continuous mask with sinusoidal^p decay over the range
344+
[alpha+s, alpha+s+w] for semiangle alpha.
345+
346+
The probe origin and convergence semiangle must be set for this
347+
method to run - these can be set using `measure_disk`. Pixels are
348+
defined as outside the central disk if their distance from the origin
349+
exceeds the semiconvergence angle * alpha_max.
350+
351+
Parameters
352+
----------
353+
s : number
354+
Damping begins at radius alpha*(1+s)
355+
w : number
356+
Width of damping window, i.e. damping ends at alpha*(1+s)+w
357+
p : number
358+
Scaling power for damping sindusoid
359+
update_inplace : bool
360+
Toggle updating the self.probe value or returning it only
361+
"""
362+
# validate inputs
363+
assert (
364+
self.alpha is not None
365+
), "no probe semiconvergence angle found; try running `Probe.measure_disk`"
366+
assert (
367+
self.origin is not None
368+
), "no probe origin found; try running `Probe.measure_disk`"
369+
# prepare vars
370+
qx0, qy0 = self.origin
371+
alpha = self.alpha
372+
probe = self.probe
373+
Q_Nx, Q_Ny = probe.shape
374+
# setup mesh
375+
qyy, qxx = np.meshgrid(np.arange(Q_Ny), np.arange(Q_Nx))
376+
qrr = np.hypot(qxx - qx0, qyy - qy0)
377+
# find damping curve
378+
r = alpha * (1 + s)
379+
_w = alpha * w
380+
f = 1 + (r - qrr) / _w
381+
f = np.minimum(np.maximum(f, 0), 1)
382+
f = np.sin(f) ** p
383+
# scale probe
384+
probe *= f
385+
# update
386+
if update_inplace:
387+
self.probe = probe
388+
# return scaled probe
389+
return probe
390+
338391
# Kernel generation methods
339392

340393
def get_kernel(
@@ -400,15 +453,9 @@ def get_kernel(
400453
# check for the origin
401454
if origin is None:
402455
try:
403-
x = self.calibration.get_probe_params()
456+
origin = self.origin
404457
except AttributeError:
405-
x = None
406-
finally:
407-
if x is None:
408-
origin = None
409-
else:
410-
r, x, y = x
411-
origin = (x, y)
458+
origin = None
412459

413460
# get the data
414461
probe = data if data is not None else self.probe
@@ -592,7 +639,7 @@ def get_probe_kernel_edge_sigmoid(
592639
np.mod(np.arange(Q_Ny) + Q_Ny // 2, Q_Ny) - Q_Ny // 2,
593640
np.mod(np.arange(Q_Nx) + Q_Nx // 2, Q_Nx) - Q_Nx // 2,
594641
)
595-
qr = np.sqrt(qx**2 + qy**2)
642+
qr = np.hypot(qx, qy)
596643
# Calculate sigmoid
597644
if type == "logistic":
598645
r0 = 0.5 * (ro + ri)

py4DSTEM/datacube/datacube.py

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
gaussian_filter,
1212
)
1313
from typing import Optional, Union
14+
import matplotlib.pyplot as plt
1415

1516
from emdfile import Array, Metadata, Node, Root, tqdmnd
1617
from py4DSTEM.data import Data, Calibration
1718
from py4DSTEM.datacube.virtualimage import DataCubeVirtualImager
1819
from py4DSTEM.datacube.virtualdiffraction import DataCubeVirtualDiffraction
20+
from py4DSTEM.visualize import show
1921

2022

2123
class DataCube(
@@ -515,11 +517,9 @@ def get_vacuum_probe(
515517
self,
516518
ROI=None,
517519
align=True,
520+
zero_vacuum=True,
518521
mask=None,
519-
threshold=0.0,
520-
expansion=12,
521-
opening=3,
522-
verbose=False,
522+
plot=True,
523523
returncalc=True,
524524
):
525525
"""
@@ -541,20 +541,20 @@ def get_vacuum_probe(
541541
(rx_min,rx_max,ry_min,ry_max)
542542
align : optional, bool
543543
if True, aligns the probes before averaging
544+
zero_vacuum : bool
545+
if True, zeros vacuum pixels outside the central probe using a
546+
sinusoidal^p decay over the range [alpha+s, alpha+s+w].
547+
s : number
548+
See zero vacuum
549+
w : number
550+
See zero vacuum
551+
p : number
552+
See zero vacuum
544553
mask : optional, array
545554
mask applied to each diffraction pattern before alignment and
546555
averaging
547-
threshold : float
548-
in the final masking step, values less than max(probe)*threshold
549-
are considered outside the probe
550-
expansion : int
551-
number of pixels by which the final mask is expanded after
552-
thresholding
553-
opening : int
554-
size of binary opening applied to the final mask to eliminate stray
555-
bright pixels
556-
verbose : bool
557-
toggles verbose output
556+
plot : bool
557+
Toggle displays
558558
returncalc : bool
559559
if True, returns the answer
560560
@@ -594,23 +594,78 @@ def get_vacuum_probe(
594594
curr_DP = get_shifted_ar(curr_DP, xshift, yshift)
595595
probe = probe * (n - 1) / n + curr_DP / n
596596

597-
# mask
598-
mask = probe > np.max(probe) * threshold
599-
mask = binary_opening(mask, iterations=opening)
600-
mask = binary_dilation(mask, iterations=1)
601-
mask = (
602-
np.cos(
603-
(np.pi / 2)
604-
* np.minimum(
605-
distance_transform_edt(np.logical_not(mask)) / expansion, 1
606-
)
597+
# make a probe
598+
probe = Probe(probe)
599+
600+
# measure probe size
601+
probe.measure_disk(zero_vacuum=False, plot=False)
602+
603+
# zero vacuum
604+
_p = probe.probe
605+
if zero_vacuum:
606+
probe.zero_vacuum_sinusoid()
607+
608+
# show
609+
if plot:
610+
fig, axs = plt.subplots(2, 3, figsize=(12, 8))
611+
show(
612+
probe.probe,
613+
scaling="log",
614+
circle={
615+
"center": probe.origin,
616+
"R": probe.alpha,
617+
"alpha": 0.2,
618+
"fill": True,
619+
},
620+
figax=(fig, axs[0, 0]),
607621
)
608-
** 2
609-
)
610-
probe *= mask
622+
show(
623+
probe.probe,
624+
scaling="none",
625+
intensity_range="minmax",
626+
circle={
627+
"center": probe.origin,
628+
"R": probe.alpha,
629+
"alpha": 0.2,
630+
"fill": True,
631+
},
632+
figax=(fig, axs[0, 1]),
633+
)
634+
show(
635+
probe.probe,
636+
scaling="none",
637+
intensity_range="absolute",
638+
vmin=0,
639+
vmax=np.max(probe.probe) * 0.1,
640+
circle={
641+
"center": probe.origin,
642+
"R": probe.alpha,
643+
"alpha": 0.2,
644+
"fill": True,
645+
},
646+
figax=(fig, axs[0, 2]),
647+
)
648+
show(probe.probe, scaling="log", figax=(fig, axs[1, 0]))
649+
show(
650+
probe.probe,
651+
scaling="none",
652+
intensity_range="minmax",
653+
figax=(fig, axs[1, 1]),
654+
)
655+
show(
656+
probe.probe,
657+
scaling="none",
658+
intensity_range="absolute",
659+
vmin=0,
660+
vmax=np.max(probe.probe) * 0.1,
661+
figax=(fig, axs[1, 2]),
662+
)
663+
axs[0, 0].set_title("log")
664+
axs[0, 1].set_title("linear | min/max")
665+
axs[0, 2].set_title("linear | min/0.1*max")
666+
plt.show()
611667

612-
# make a probe, add to tree, and return
613-
probe = Probe(probe)
668+
# add probe to tree and return
614669
self.attach(probe)
615670
if returncalc:
616671
return probe

py4DSTEM/process/diffraction/digital_dark_field.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
A general note on all these functions is that they are designed for use with rotation calibration into the pointslist.
3-
However, they have to date only been used with the Qx and Qy in pixels and not calibrated into reciprocal units.
3+
However, they have to date only been used with the Qx and Qy in pixels and not calibrated into reciprocal units.
44
There is no reason why this should not work, but the default tolerance would need adjustment.
55
"""
66

py4DSTEM/process/polar/polar_analysis.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ def calculate_radial_statistics(
2828
compute the mean and standard deviation pattern by pattern, i.e. for
2929
diffraction signal d(x,y; q,theta) we take
3030
31-
d_mean_all(x,y; q) = \int_{0}^{2\pi} d(x,y; q,\theta) d\theta
32-
d_var_all(x,y; q) = \int_{0}^{2\pi}
33-
\( d(x,y; q,\theta) - d_mean_all(x,y; q,\theta) \)^2 d\theta
31+
d_mean_all(x,y; q) = \\int_{0}^{2\\pi} d(x,y; q,\\theta) d\\theta
32+
d_var_all(x,y; q) = \\int_{0}^{2\\pi}
33+
\\( d(x,y; q,\\theta) - d_mean_all(x,y; q,\\theta) \\)^2 d\\theta
3434
3535
Then we find the mean and variance profiles by taking the means of these
3636
quantities over all scan positions:
3737
38-
d_mean(q) = \sum_{x,y} d_mean_all(x,y; q)
39-
d_var(q) = \sum_{x,y} d_var_all(x,y; q)
38+
d_mean(q) = \\sum_{x,y} d_mean_all(x,y; q)
39+
d_var(q) = \\sum_{x,y} d_var_all(x,y; q)
4040
4141
and the normalized variance is d_var/d_mean.
4242
@@ -264,13 +264,13 @@ def calculate_pair_dist_function(
264264
filters are optionally applied. The structure factor is then inverted into
265265
the reduced pair distribution function g(r) using
266266
267-
g(r) = \frac{2}{\pi) \int sin( 2\pi r k ) S(k) dk
267+
g(r) = \\frac{2}{\\pi) \\int sin( 2\\pi r k ) S(k) dk
268268
269269
The value of the integral is (optionally) damped to zero at the origin to
270270
match the physical requirement that this condition holds. Finally, the
271271
full PDF G(r) is computed if a known density is provided, using
272272
273-
G(r) = 1 + [ \frac{2}{\pi} * g(r) / ( 4\pi * D * r dr ) ]
273+
G(r) = 1 + [ \\frac{2}{\\pi} * g(r) / ( 4\\pi * D * r dr ) ]
274274
275275
This follows the methods described in [@cophus TODO ADD CITATION].
276276

0 commit comments

Comments
 (0)