Demonstrate hklpy2’s API#

A working notebook as the package is being developed. Anything could change.

Load the hklpy2 package and show basic information about it.

[1]:
import datetime
import hklpy2
import math
from pprint import pprint

print(f"{datetime.datetime.now()}")
print(f"{hklpy2.__version__=}")
print(f"{hklpy2.solvers()=}")
2024-07-29 09:58:21.160093
hklpy2.__version__='0.0.25.dev40+g72b5421.d20240729'
hklpy2.solvers()={'hkl_soleil': 'hklpy2.backends.hkl_soleil:HklSolver', 'no_op': 'hklpy2.backends.no_op:NoOpSolver', 'th_tth': 'hklpy2.backends.th_tth_q:ThTthSolver'}

Create the simulated E4CV (4-circle) diffractometer from the "hkl_soleil" solver.

[2]:
from hklpy2 import SimulatedE4CV

sim4c = SimulatedE4CV(name="sim4c")
print(f"{sim4c.solver.get()=}")
print(f"{sim4c.geometry.get()=}")
print(f"{sim4c.sample=}")
pprint(sim4c.operator.axes_xref)
print(f"{sim4c.position=}")
sim4c.solver.get()='hkl_soleil'
sim4c.geometry.get()='E4CV'
sim4c.sample=Sample(name='sample', lattice=Lattice(a=1, system='cubic'))
{'chi': 'chi',
 'h': 'h',
 'k': 'k',
 'l': 'l',
 'omega': 'omega',
 'phi': 'phi',
 'tth': 'tth'}
sim4c.position=SimulatedE4CVPseudoPos(h=0, k=0, l=0)

Create a \(\theta-2\theta\) 2-circle diffractometer using "th_tth", a different backend solver. This demonstrates the ability to choose between different backend solvers.

The "th_tth" solver was written in Python to demonstrate this new capability as a design goal for hklpy2.

[3]:
from hklpy2 import SimulatedTheta2Theta

powder = SimulatedTheta2Theta(name="powder")
print(f"{powder.solver.get()=}")
print(f"{powder.geometry.get()=}")
print(f"{powder.sample=}")
print(f"{powder.operator.axes_xref=}")
print(f"{powder.position=}")
powder.solver.get()='th_tth'
powder.geometry.get()='TH TTH Q'
powder.sample=Sample(name='sample', lattice=Lattice(a=1, system='cubic'))
powder.operator.axes_xref={'q': 'q', 'theta': 'th', 'ttheta': 'tth'}
powder.position=SimulatedTheta2ThetaPseudoPos(q=0)

SimulatedE4CV#

[4]:
fourc = hklpy2.SimulatedE4CV(name="fourc")

Add a sample, as in hklpy.

[5]:
print(f"{fourc.samples=}")
print(f"{fourc.sample=}")
fourc.operator.remove_sample("vibranium",)  # just in case it was defined previously
fourc.add_sample("vibranium", 2*math.pi, digits=3, replace=True)  # or force a replacement
print(f"{fourc.sample=}")
print(f"{fourc.samples=}")
fourc.sample = "sample"  # switch back to the default sample
print(f"{fourc.sample=}")
fourc.samples={'sample': Sample(name='sample', lattice=Lattice(a=1, system='cubic'))}
fourc.sample=Sample(name='sample', lattice=Lattice(a=1, system='cubic'))
fourc.sample=Sample(name='vibranium', lattice=Lattice(a=6.283, system='cubic'))
fourc.samples={'sample': Sample(name='sample', lattice=Lattice(a=1, system='cubic')), 'vibranium': Sample(name='vibranium', lattice=Lattice(a=6.283, system='cubic'))}
fourc.sample=Sample(name='sample', lattice=Lattice(a=1, system='cubic'))
[6]:
fourc.report
[6]:
{'position': SimulatedE4CVPseudoPos(h=0, k=0, l=0)}
[7]:
fourc.sample = "vibranium"
fourc.sample.reflections.order = []

Add a couple reflections (with the eventual goal of calculating the \(UB\) matrix).

[8]:
fourc.add_reflection((1, 0, 0), (10, 0, 0, 20), name="r1")
fourc.add_reflection((0, 1, 0), (10, -90, 0, 20), name="r2")
for r in fourc.sample.reflections.order:
    print(f"{fourc.sample.reflections[r]}")
fourc.operator.calcUB(*fourc.sample.reflections.order)
print(f"{fourc.operator.solver.U=!r}")
print(f"{fourc.operator.solver.UB=!r}")
print(f"{fourc.inverse(10, 0, 0, 20)=!r}")
Reflection(name='r1', geometry='E4CV', pseudos={'h': 1, 'k': 0, 'l': 0}, reals={'omega': 10, 'chi': 0, 'phi': 0, 'tth': 20}, wavelength=1.0)
Reflection(name='r2', geometry='E4CV', pseudos={'h': 0, 'k': 1, 'l': 0}, reals={'omega': 10, 'chi': -90, 'phi': 0, 'tth': 20}, wavelength=1.0)
fourc.operator.solver.U=[[-0.0, -0.0, 1.0], [0.0, -1.0, -0.0], [1.0, -0.0, 0.0]]
fourc.operator.solver.UB=[[-0.0, -0.0, 1.0], [0.0, -1.0, 0.0], [1.0, -0.0, -0.0]]
fourc.inverse(10, 0, 0, 20)=SimulatedE4CVPseudoPos(h=2.182127357071, k=0, l=0)

Swap the first two reflections.

[9]:
fourc.sample.reflections.swap()
print(f"{fourc.sample.reflections.order=}")
for r in fourc.sample.reflections.order:
    print(f"{fourc.sample.reflections[r]}")
print(f"{fourc.operator.solver.U=!r}")
print(f"{fourc.operator.solver.UB=!r}")
print(f"{fourc.forward(1, 0, 0)=!r}")
print(f"{fourc.inverse(10, 0, 0, 20)=!r}")
fourc.sample.reflections.order=['r2', 'r1']
Reflection(name='r2', geometry='E4CV', pseudos={'h': 0, 'k': 1, 'l': 0}, reals={'omega': 10, 'chi': -90, 'phi': 0, 'tth': 20}, wavelength=1.0)
Reflection(name='r1', geometry='E4CV', pseudos={'h': 1, 'k': 0, 'l': 0}, reals={'omega': 10, 'chi': 0, 'phi': 0, 'tth': 20}, wavelength=1.0)
fourc.operator.solver.U=[[-0.0, -0.0, 1.0], [0.0, -1.0, -0.0], [1.0, -0.0, 0.0]]
fourc.operator.solver.UB=[[-0.0, -0.0, 1.0], [0.0, -1.0, 0.0], [1.0, -0.0, -0.0]]
fourc.forward(1, 0, 0)=SimulatedE4CVRealPos(omega=4.564279207751, chi=0, phi=-2.0227e-08, tth=9.128558415503)
fourc.inverse(10, 0, 0, 20)=SimulatedE4CVPseudoPos(h=2.182127357071, k=0, l=0)

4-circle with extra axes#

Construct a 4-circle diffractometer with many additional axes, both in real (rotation angle) space and in reciprocal (pseudo) space.

[10]:
from ophyd import Component as Cpt
from ophyd import Kind
from ophyd import PseudoSingle
from ophyd import SoftPositioner

NORMAL_HINTED = Kind.hinted | Kind.normal

class Fourc(hklpy2.DiffractometerBase):
    """Test case."""

    # pseudo-space axes, in order expected by hkl_soleil E4CV, engine="hkl"
    h = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741
    k = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741
    l = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741

    # real-space axes, in order expected by hkl_soleil E4CV
    # using different names
    theta = Cpt(SoftPositioner, limits=(-180, 180), init_pos=0, kind=NORMAL_HINTED)
    chi = Cpt(SoftPositioner, limits=(-180, 180), init_pos=0, kind=NORMAL_HINTED)
    phi = Cpt(SoftPositioner, limits=(-180, 180), init_pos=0, kind=NORMAL_HINTED)
    ttheta = Cpt(SoftPositioner, limits=(-170, 170), init_pos=0, kind=NORMAL_HINTED)

    # pseudo-space extra axes used in a couple modes
    h2 = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741
    k2 = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741
    l2 = Cpt(PseudoSingle, "", kind=NORMAL_HINTED)  # noqa: E741

    # real-space extra axis used in a couple modes
    psi = Cpt(SoftPositioner, limits=(-170, 170), init_pos=0, kind=NORMAL_HINTED)

    # another Component, not used (yet)
    energy = Cpt(SoftPositioner, limits=(5, 35), init_pos=12.4, kind=NORMAL_HINTED)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, solver="hkl_soleil", geometry="E4CV", solver_kwargs=dict(engine="hkl"), **kwargs)
        self.operator.auto_assign_axes()

fourc = Fourc(name="fourc")
print(f"{fourc=}")
fourc=Fourc(prefix='', name='fourc', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed', read_attrs=['h', 'h.readback', 'h.setpoint', 'k', 'k.readback', 'k.setpoint', 'l', 'l.readback', 'l.setpoint', 'theta', 'chi', 'phi', 'ttheta', 'h2', 'h2.readback', 'h2.setpoint', 'k2', 'k2.readback', 'k2.setpoint', 'l2', 'l2.readback', 'l2.setpoint', 'psi', 'energy'], configuration_attrs=['geometry', 'solver', 'wavelength', 'h', 'k', 'l', 'h2', 'k2', 'l2'], concurrent=True)

Next steps demonstrate some additional design goals:

  • Easy to add additional axes, such as \(\psi\), \(h_2\), \(k_2\), & \(l_2\).

    • Even axes, such as energy, that are not used directly but may be interesting to include.

  • Support for axes used as extra parameters in various diffractometer modes.

  • User can specify which axes are to be used by the solver.

  • Automatic selection of pseudo and real axes (based on order of appearance).

  • User can choose any names for their axes.

  • Solver class provides some introspection:

    • name and version

    • geometries supported

    • axes and parameters used by a geometry and mode

[11]:
print(f"{fourc.operator.solver.geometries()=}")
fourc.operator.solver.geometries()=['APS POLAR', 'E4CH', 'E4CV', 'E6C', 'ESRF ID01 PSIC', 'K4CV', 'K6C', 'PETRA3 P09 EH2', 'PETRA3 P23 4C', 'PETRA3 P23 6C', 'SOLEIL MARS', 'SOLEIL NANOSCOPIUM ROBOT', 'SOLEIL SIRIUS KAPPA', 'SOLEIL SIRIUS TURRET', 'SOLEIL SIXS MED1+2', 'SOLEIL SIXS MED2+2', 'SOLEIL SIXS MED2+3', 'SOLEIL SIXS MED2+3 v2', 'TwoC', 'ZAXIS']
[12]:
print(f"{fourc.solver.get()=}")
print(f"{fourc.geometry.get()=}")
print(f"{fourc.wavelength.get()=}")
fourc.solver.get()='hkl_soleil'
fourc.geometry.get()='E4CV'
fourc.wavelength.get()=1.0
[13]:
print(f"{fourc.solver_name=}")
print(f"{fourc.operator.solver=}")
print(f"{fourc.operator.axes_xref=!r}")  # our names to solver's names
print(f"{fourc.pseudo_axis_names=}")  # our full ordered lists of names
print(f"{fourc.real_axis_names=}")
print(f"{fourc.operator.solver.pseudo_axis_names=}")  # solver's ordered lists of names
print(f"{fourc.operator.solver.real_axis_names=}")
print(f"{fourc.operator.solver.extra_axis_names=}")
fourc.solver_name='hkl_soleil'
fourc.operator.solver=HklSolver(name='hkl_soleil', version='5.0.0.3511', geometry='E4CV', engine_name='hkl', mode='bissector')
fourc.operator.axes_xref={'h': 'h', 'k': 'k', 'l': 'l', 'theta': 'omega', 'chi': 'chi', 'phi': 'phi', 'ttheta': 'tth'}
fourc.pseudo_axis_names=['h', 'k', 'l', 'h2', 'k2', 'l2']
fourc.real_axis_names=['theta', 'chi', 'phi', 'ttheta', 'psi', 'energy']
fourc.operator.solver.pseudo_axis_names=['h', 'k', 'l']
fourc.operator.solver.real_axis_names=['omega', 'chi', 'phi', 'tth']
fourc.operator.solver.extra_axis_names=[]

Where is the diffractometer now?

[15]:
fourc.wh()
wavelength=1.0
h=0 k=0 l=0 h2=0 k2=0 l2=0
theta=0 chi=0 phi=0 ttheta=0 psi=0 energy=12.4

Show ophyd’s description of the diffractometer object.

[14]:
fourc.summary()
data keys (* hints)
-------------------
*fourc_chi
*fourc_energy
*fourc_h
*fourc_h2
 fourc_h2_setpoint
 fourc_h_setpoint
*fourc_k
*fourc_k2
 fourc_k2_setpoint
 fourc_k_setpoint
*fourc_l
*fourc_l2
 fourc_l2_setpoint
 fourc_l_setpoint
*fourc_phi
*fourc_psi
*fourc_theta
*fourc_ttheta

read attrs
----------
h                    PseudoSingle        ('fourc_h')
h.readback           AttributeSignal     ('fourc_h')
h.setpoint           AttributeSignal     ('fourc_h_setpoint')
k                    PseudoSingle        ('fourc_k')
k.readback           AttributeSignal     ('fourc_k')
k.setpoint           AttributeSignal     ('fourc_k_setpoint')
l                    PseudoSingle        ('fourc_l')
l.readback           AttributeSignal     ('fourc_l')
l.setpoint           AttributeSignal     ('fourc_l_setpoint')
theta                SoftPositioner      ('fourc_theta')
chi                  SoftPositioner      ('fourc_chi')
phi                  SoftPositioner      ('fourc_phi')
ttheta               SoftPositioner      ('fourc_ttheta')
h2                   PseudoSingle        ('fourc_h2')
h2.readback          AttributeSignal     ('fourc_h2')
h2.setpoint          AttributeSignal     ('fourc_h2_setpoint')
k2                   PseudoSingle        ('fourc_k2')
k2.readback          AttributeSignal     ('fourc_k2')
k2.setpoint          AttributeSignal     ('fourc_k2_setpoint')
l2                   PseudoSingle        ('fourc_l2')
l2.readback          AttributeSignal     ('fourc_l2')
l2.setpoint          AttributeSignal     ('fourc_l2_setpoint')
psi                  SoftPositioner      ('fourc_psi')
energy               SoftPositioner      ('fourc_energy')

config keys
-----------
fourc_geometry
fourc_solver
fourc_wavelength

configuration attrs
-------------------
geometry             AttributeSignal     ('fourc_geometry')
solver               AttributeSignal     ('fourc_solver')
wavelength           AttributeSignal     ('fourc_wavelength')
h                    PseudoSingle        ('fourc_h')
k                    PseudoSingle        ('fourc_k')
l                    PseudoSingle        ('fourc_l')
h2                   PseudoSingle        ('fourc_h2')
k2                   PseudoSingle        ('fourc_k2')
l2                   PseudoSingle        ('fourc_l2')

unused attrs
------------