Zone Scan#
A crystallographic zone is characterized by a set of planes which are all parallel to one line, called the zone axis. (*) If the axis of a zone has indices \([uvw]\), then any plane belongs to that zone whose indices \((hkl)\) satisfy the relation:
\(hu+kv+lw=0\)
In diffractometer use, a zone is used to align, index, and interpret diffraction patterns. Key uses and practical steps:
Purpose#
Crystal Alignment: Orient the sample so a chosen zone axis is parallel to the incident beam or goniometer rotation axis. That places high symmetry directions into the diffractometer frame and often maximizes systematic reflections.
Indexing: When the crystal is on a zone axis, all observed diffraction spots correspond to reflections whose reciprocal-lattice vectors lie in a plane perpendicular to that axis. This simplifies assignment of Miller indices.
Symmetry determination: A zone-axis pattern reveals symmetry of the lattice projection, helping determine lattice type, possible space groups, and systematic absences.
Setting up data collection: Choosing zone axes for multiple orientations (e.g., principal axes) helps plan rotation ranges and ensures coverage of independent reflections.
(*) Elements of X-ray Diffraction, B.D. Cullity, 1978, Second Edition.
import numpy as np
from hklpy2.blocks.zone import OrthonormalZone, zone_series, scan_zone
np.set_printoptions(precision=4, suppress=True, floatmode="maxprec")
v1 = np.array((1, 1, 0))
v2 = np.array((-3, -2, -1))
v3 = np.array((-1, 1, 1))
zone = OrthonormalZone()
print(f"{zone=}")
zone.axis = v3
print(f"{zone=}")
print(f"{OrthonormalZone(axis=v1)=}")
print(f"{OrthonormalZone(axis=[0, 1, 1])=}")
print(f"{OrthonormalZone(axis=(1, 2, 3))=}")
print(f"{OrthonormalZone(axis=dict(u=11, v=22, w=33))=}")
print(f"{OrthonormalZone(b1=v1, b2=v2)=}")
print(f"{zone.in_zone(v1)=}")
print(f"{zone.in_zone(v2)=}")
print(f"{zone.in_zone(v3)=}")
print(f"{v1=}")
print(f"{v2=}")
zone=OrthonormalZone(axis='undefined')
zone=OrthonormalZone(axis=array([-0.5774, 0.5774, 0.5774]))
OrthonormalZone(axis=v1)=OrthonormalZone(axis=array([0.7071, 0.7071, 0. ]))
OrthonormalZone(axis=[0, 1, 1])=OrthonormalZone(axis=array([0. , 0.7071, 0.7071]))
OrthonormalZone(axis=(1, 2, 3))=OrthonormalZone(axis=array([0.2673, 0.5345, 0.8018]))
OrthonormalZone(axis=dict(u=11, v=22, w=33))=OrthonormalZone(axis=array([0.2673, 0.5345, 0.8018]))
OrthonormalZone(b1=v1, b2=v2)=OrthonormalZone(axis=array([-0.5774, 0.5774, 0.5774]))
zone.in_zone(v1)=True
zone.in_zone(v2)=True
zone.in_zone(v3)=False
v1=array([1, 1, 0])
v2=array([-3, -2, -1])
import hklpy2
sim = hklpy2.creator()
print(f"{sim.forward([-0.5, -1., 1.])=}")
print(f"{sim.forward([0, -1, 0.5])=}")
sim.forward([-0.5, -1., 1.])=Hklpy2DiffractometerRealPos(omega=48.5904, chi=-41.8103, phi=-26.5651, tth=97.1808)
sim.forward([0, -1, 0.5])=Hklpy2DiffractometerRealPos(omega=33.9879, chi=-63.435, phi=-0.0, tth=67.9757)
sim.core.constraints["tth"].limits = -0.01, 180.01
zone_series(sim, (1, 0, 0), [0, 1, 0], 5)
# zone_series(sim, v1, v3, 17)
# zone_series(sim, (1, 0, 0), [0, -1, 0.5], 17)
hkl_1=(1, 0, 0) hkl_2=[0, 1, 0] n=5
====== ====== ====== ======= ======= ======= =======
h k l omega chi phi tth
====== ====== ====== ======= ======= ======= =======
1.0000 0.0000 0.0000 30.0000 0.0000 90.0000 60.0000
0.9239 0.3827 0.0000 30.0000 22.5000 90.0000 60.0000
0.7071 0.7071 0.0000 30.0000 45.0000 90.0000 60.0000
0.3827 0.9239 0.0000 30.0000 67.5000 90.0000 60.0000
0.0000 1.0000 0.0000 30.0000 90.0000 0.0000 60.0000
====== ====== ====== ======= ======= ======= =======
sim.add_sample("test", 4, 5, 6, 75, 85, 95, replace=True)
r1 = sim.add_reflection((4, 0, 0), (30.345, 10, 10, 60.69))
r2 = sim.add_reflection((0, 4, 0), (-24.63, -9.265, -85.08, -49.27))
np.asarray(sim.core.calc_UB(r1, r2), dtype=float)
array([[ 0.2714, 1.2877, -0.4837],
[ 0.2756, 0.2108, 0.9575],
[ 1.5393, -0.1109, -0.2144]])
zone_series(sim, (4, 0, 0), [0, 4, 0], 5)
hkl_1=(4, 0, 0) hkl_2=[0, 4, 0] n=5
====== ====== ====== ======= ======= ======= =======
h k l omega chi phi tth
====== ====== ====== ======= ======= ======= =======
4.0000 0.0000 0.0000 30.3449 10.0000 10.0000 60.6899
4.0398 1.3746 0.0000 33.0559 11.8130 25.2925 66.1118
3.3304 2.6643 0.0000 31.9896 12.8420 41.8998 63.9792
1.9204 3.6121 0.0000 28.0637 12.6122 63.7067 56.1274
0.0000 4.0000 0.0000 24.6345 9.2657 94.9203 49.2691
====== ====== ====== ======= ======= ======= =======
from bluesky import RunEngine
from bluesky.callbacks.best_effort import BestEffortCallback
from ophyd.sim import noisy_det
RE = RunEngine()
bec = BestEffortCallback()
bec.disable_plots()
RE.subscribe(bec)
0
uid, = RE(scan_zone([noisy_det], sim, (1,0,0), (0,1,0), 5))
Transient Scan ID: 1 Time: 2025-12-11 10:51:40
Persistent Unique Scan ID: '07cb65c6-8065-49a7-86a8-b11aa4553144'
New stream: 'primary'
+-----------+------------+------------+----------------------+------------------+------------+------------+------------+------------+------------+------------+------------+
| seq_num | time | noisy_det | e4cv_beam_wavelength | e4cv_beam_energy | e4cv_h | e4cv_k | e4cv_l | e4cv_omega | e4cv_chi | e4cv_phi | e4cv_tth |
+-----------+------------+------------+----------------------+------------------+------------+------------+------------+------------+------------+------------+------------+
| 1 | 10:51:40.5 | 0.973 | 1.000 | 12.398 | 1.000 | 0.000 | -0.000 | 7.256 | 10.000 | 10.000 | 14.512 |
| 2 | 10:51:40.5 | 0.958 | 1.000 | 12.398 | 1.010 | 0.344 | 0.000 | 7.838 | 11.813 | 25.293 | 15.675 |
| 3 | 10:51:40.5 | 1.058 | 1.000 | 12.398 | 0.833 | 0.666 | -0.000 | 7.611 | 12.842 | 41.900 | 15.221 |
| 4 | 10:51:40.5 | 0.919 | 1.000 | 12.398 | 0.480 | 0.903 | 0.000 | 6.754 | 12.612 | 63.707 | 13.509 |
| 5 | 10:51:40.5 | 1.057 | 1.000 | 12.398 | 0.000 | 1.000 | -0.000 | 5.981 | 9.266 | 94.920 | 11.963 |
+-----------+------------+------------+----------------------+------------------+------------+------------+------------+------------+------------+------------+------------+
Plan ['07cb65c6'] (scan num: 1)