Polarization Analyzer on the Detector Arm#
A polarization analyzer (often just polarizer) is a passive optical
element on the detector arm. It is a crystal analyzer chosen so that the
Bragg angle is close to 45° — i.e. the scattering angle 2θ is near 90° —
which suppresses radiation polarized in the diffraction plane. Rotating
the polarizer’s diffraction plane around the beam (the peta axis)
selects which polarization direction is suppressed, allowing the beam
polarization state to be probed.
The control system operates the polarizer motors; hklpy2 treats them as auxiliary positioners that are read alongside the diffractometer but do not affect reciprocal-space calculations.
Objective
Add three polarization-analyzer axes (pth, ptth, peta) to an E4CV
diffractometer, demonstrate their use in a Bluesky session, and show how
to save and restore the diffractometer configuration.
Polarizer axes (nominal names used here)
axis |
description |
|---|---|
|
polarizer crystal Bragg angle (θ) |
|
polarizer detector arm angle (2θ ≈ 90°) |
|
rotation of the polarizer diffraction plane around the beam |
These names are conventional; substitute the names used at your beamline.
Some installations add a fourth motor (pchi) for crystal alignment; it
is added to reals exactly like the others and is otherwise invisible to
the diffractometer.
Scope. Only polarizers mounted between the sample and the detector are in scope. Upstream (incident-beam) polarizers are out of scope for hklpy2.
Setup#
Create an E4CV diffractometer with the three polarizer axes declared
alongside the four standard real motors. Pass all seven axes in the
reals dictionary; the four that the solver knows about (omega,
chi, phi, tth) are mapped automatically; the remaining three
(pth, ptth, peta) appear as auxiliaries.
The solver axes (omega, chi, phi, tth) must appear in the order
the solver expects — giving them out of order will cause incorrect motor
mapping. Auxiliary axes may be appended in any order after the solver
axes. See the Diffractometer axes guide for details.
import hklpy2
e4cv = hklpy2.creator(
name="e4cv",
reals=dict(
omega=None, # simulated motor
chi=None,
phi=None,
tth=None,
pth=None, # polarizer theta
ptth=None, # polarizer 2-theta
peta=None, # rotation about the beam
),
)
Show the diffractometer position summary. The polarizer axes appear under auxiliaries.
e4cv.wh()
wavelength=1.0
pseudos: h=0, k=0, l=0
reals: omega=0, chi=0, phi=0, tth=0
auxiliaries: pth=0, ptth=0, peta=0
Show all ophyd component names.
print(f"{e4cv.component_names=}")
e4cv.component_names=('beam', 'h', 'k', 'l', 'omega', 'chi', 'phi', 'tth', 'pth', 'ptth', 'peta')
Orient the diffractometer#
Add a sample and two orientation reflections, then compute the UB matrix. The polarizer axes play no role in orientation.
e4cv.core.add_sample("vibranium", 4.04)
r1 = e4cv.core.add_reflection(
dict(h=4, k=0, l=0),
dict(omega=-145.451, chi=0, phi=0, tth=69.066),
)
r2 = e4cv.core.add_reflection(
dict(h=0, k=4, l=0),
dict(omega=-145.451, chi=0, phi=90, tth=69.066),
)
e4cv.core.calc_UB(r1, r2)
print("UB matrix computed.")
UB matrix computed.
Set the wavelength and position the polarizer near 90°#
The polarizer crystal is chosen so that its Bragg angle for the incident
wavelength is close to 45°, putting ptth near 90°. The example below
uses pyrolytic graphite PG(002) with d-spacing 3.355 Å; a wavelength
of 4.7 Å gives pth ≈ 44.5° and ptth ≈ 89°.
At a real beamline the wavelength is typically determined by the
monochromator and is read-only; the polarizer crystal and pth/ptth
are chosen accordingly.
import math
d_spacing = 3.355 # PG(002) d-spacing in angstroms
# Set incident wavelength. At a beamline this may be read-only.
e4cv.beam.wavelength.put(4.7) # angstroms
# Compute polarizer Bragg angle (pth) and arm angle (ptth ≈ 90°).
wavelength = e4cv.beam.wavelength.get()
e4cv.pth.move(math.degrees(math.asin(wavelength / (2 * d_spacing))))
e4cv.ptth.move(e4cv.pth.position * 2)
e4cv.peta.move(0)
print(f"incident wavelength : {wavelength:.4f} Å")
print(f"PG(002) d-spacing : {d_spacing:.4f} Å")
print(f"pth : {e4cv.pth.position:.4f} deg")
print(f"ptth : {e4cv.ptth.position:.4f} deg")
print(f"peta : {e4cv.peta.position:.4f} deg")
e4cv.wh()
incident wavelength : 4.7000 Å
PG(002) d-spacing : 3.3550 Å
pth : 44.4629 deg
ptth : 88.9258 deg
peta : 0.0000 deg
wavelength=4.7
pseudos: h=0, k=0, l=0
reals: omega=0, chi=0, phi=0, tth=0
auxiliaries: pth=44.4629, ptth=88.9258, peta=0
Rotate the diffraction plane around the beam#
With ptth close to 90°, radiation polarized in the polarizer’s
diffraction plane is suppressed. Rotating peta rotates that
diffraction plane around the beam, selecting which linear polarization
component is suppressed. pth and ptth are unchanged by peta.
The cells below move peta to several positions. In a real session
this would be wrapped in a Bluesky plan (e.g. bp.scan over peta)
and the detector signal would be recorded at each step.
for angle in (0, 45, 90):
e4cv.peta.move(angle)
print(
f"peta = {e4cv.peta.position:6.2f} deg | "
f"pth = {e4cv.pth.position:.4f} | "
f"ptth = {e4cv.ptth.position:.4f}"
)
peta = 0.00 deg | pth = 44.4629 | ptth = 88.9258
peta = 45.00 deg | pth = 44.4629 | ptth = 88.9258
peta = 90.00 deg | pth = 44.4629 | ptth = 88.9258
Read all axes together#
Because the polarizer axes are ophyd components on the diffractometer
device, they are included in read() and will be recorded in every
Bluesky run document automatically.
e4cv.read()
OrderedDict([('e4cv_beam_wavelength',
{'value': 4.7, 'timestamp': 1777396392.54133}),
('e4cv_beam_energy',
{'value': 2.63796166890571, 'timestamp': 1777396392.2805746}),
('e4cv_h', {'value': 0, 'timestamp': 1777396392.2809193}),
('e4cv_h_setpoint',
{'value': 0, 'timestamp': 1777396392.2809606}),
('e4cv_k', {'value': 0, 'timestamp': 1777396392.2811275}),
('e4cv_k_setpoint',
{'value': 0, 'timestamp': 1777396392.2811594}),
('e4cv_l', {'value': 0, 'timestamp': 1777396392.281286}),
('e4cv_l_setpoint',
{'value': 0, 'timestamp': 1777396392.2813118}),
('e4cv_omega', {'value': 0, 'timestamp': 1777396392.5514412}),
('e4cv_chi', {'value': 0, 'timestamp': 1777396392.5514452}),
('e4cv_phi', {'value': 0, 'timestamp': 1777396392.551448}),
('e4cv_tth', {'value': 0, 'timestamp': 1777396392.5514514}),
('e4cv_pth',
{'value': 44.462885420892945, 'timestamp': 1777396392.5514534}),
('e4cv_ptth',
{'value': 88.92577084178589, 'timestamp': 1777396392.5514557}),
('e4cv_peta', {'value': 90, 'timestamp': 1777396392.551458})])
Verify the polarizer wavelength#
Back-compute the polarizer wavelength from the current pth position
using Bragg’s law (λ = 2 d sin θ) and confirm it matches the incident
wavelength.
polarizer_wavelength = 2 * d_spacing * math.sin(math.radians(e4cv.pth.position))
print(f"pth : {e4cv.pth.position:.4f} deg")
print(f"Polarizer wavelength : {polarizer_wavelength:.4f} Å")
print(f"Incident wavelength : {e4cv.beam.wavelength.get():.4f} Å")
pth : 44.4629 deg
Polarizer wavelength : 4.7000 Å
Incident wavelength : 4.7000 Å
Save the diffractometer configuration#
Export the diffractometer configuration (orientation, sample,
reflections, constraints) to a YAML file. The auxiliary axis names
(pth, ptth, peta) are recorded in the file so the diffractometer
can be rebuilt with the same axes. Auxiliary axis positions are not
stored and must be restored separately (e.g. from EPICS PV readback, or
by moving them explicitly after restore).
config_file = "e4cv-polarizer.yml"
e4cv.export(config_file, comment="E4CV with polarizer axes example")
print(f"Configuration saved to {config_file}")
Configuration saved to e4cv-polarizer.yml
Restore the configuration#
Restore the diffractometer from the saved file. The auxiliary axes
(pth, ptth, peta) are recreated automatically from the
configuration — no reals= argument is needed. Their positions
default to zero and must be set explicitly.
e4cv2 = hklpy2.simulator_from_config(config_file)
e4cv2.wh()
wavelength=4.7
pseudos: h=0, k=0, l=0
reals: omega=0, chi=0, phi=0, tth=0
auxiliaries: pth=0, ptth=0, peta=0
The orientation (UB matrix, sample, reflections) and the solver mode are fully restored. Two items are not stored in the configuration file and must be set explicitly as part of session startup:
Polarizer positions — default to zero; move them as shown above.
Incident wavelength — restored to the value at export time, but at a beamline this is typically driven live by the monochromator.
(The peta rotation around the beam is also independent of the
diffractometer orientation; it can be set at any time without affecting
forward()/inverse().)
Verify the restored orientation#
Confirm that forward() gives the same result on both the original and
restored diffractometers.
hkl = dict(h=1, k=0, l=0)
print(f"original: forward{hkl} -> {e4cv.forward(**hkl)[0]:.4f}")
print(f"restored: forward{hkl} -> {e4cv2.forward(**hkl)[0]:.4f}")
original: forward{'h': 1, 'k': 0, 'l': 0} -> -35.5690
restored: forward{'h': 1, 'k': 0, 'l': 0} -> -35.5690
Optional fourth axis: pchi#
Some installations add a fourth motor (pchi) for crystal alignment.
It is added to reals exactly like the others:
e4cv = hklpy2.creator(
name="e4cv",
reals=dict(
omega=None, chi=None, phi=None, tth=None,
pth=None, ptth=None, peta=None, pchi=None,
),
)
pchi is invisible to the diffractometer crystallography and is read,
saved, and restored alongside the other auxiliaries.
Clean up the example file#
import pathlib
pathlib.Path(config_file).unlink(missing_ok=True)