Skip to content

Commit eb3b65f

Browse files
authored
Merge pull request #619 from scipp/inelastic-coord-transforms
Add function to compute wavevector
2 parents cfd10e2 + 0489227 commit eb3b65f

File tree

3 files changed

+115
-49
lines changed

3 files changed

+115
-49
lines changed

src/scippneutron/conversion/graph/tof.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@
3131
'tof': {
3232
'dspacing': _kernels.dspacing_from_tof,
3333
'energy': _kernels.energy_from_tof,
34-
'hkl_vec': _kernels.hkl_vec_from_Q_vec,
34+
'hkl_vec': _kernels.hkl_vec_from_elastic_Q_vec,
3535
('h', 'k', 'l'): _kernels.hkl_elements_from_hkl_vec,
3636
'ub_matrix': _kernels.ub_matrix_from_u_and_b,
37-
'Q': _kernels.Q_from_wavelength,
38-
'Q_vec': _kernels.Q_vec_from_Q_elements,
39-
('Qx', 'Qy', 'Qz'): _kernels.Q_elements_from_wavelength,
37+
'Q': _kernels.elastic_Q_from_wavelength,
38+
'Q_vec': _kernels.elastic_Q_vec_from_Q_elements,
39+
('Qx', 'Qy', 'Qz'): _kernels.elastic_Q_elements_from_wavelength,
4040
'wavelength': _kernels.wavelength_from_tof,
4141
'time_at_sample': _kernels.time_at_sample_from_tof,
4242
},
@@ -46,12 +46,12 @@
4646
'wavelength': {
4747
'dspacing': _kernels.dspacing_from_wavelength,
4848
'energy': _kernels.energy_from_wavelength,
49-
'hkl_vec': _kernels.hkl_vec_from_Q_vec,
49+
'hkl_vec': _kernels.hkl_vec_from_elastic_Q_vec,
5050
('h', 'k', 'l'): _kernels.hkl_elements_from_hkl_vec,
5151
'ub_matrix': _kernels.ub_matrix_from_u_and_b,
52-
'Q': _kernels.Q_from_wavelength,
53-
'Q_vec': _kernels.Q_vec_from_Q_elements,
54-
('Qx', 'Qy', 'Qz'): _kernels.Q_elements_from_wavelength,
52+
'Q': _kernels.elastic_Q_from_wavelength,
53+
'Q_vec': _kernels.elastic_Q_vec_from_Q_elements,
54+
('Qx', 'Qy', 'Qz'): _kernels.elastic_Q_elements_from_wavelength,
5555
},
5656
}
5757

src/scippneutron/conversion/tof.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,49 @@ def wavelength_from_energy(*, energy: Variable) -> Variable:
312312
return sc.sqrt(c / energy)
313313

314314

315-
def _wavelength_Q_conversions(x: Variable, two_theta: Variable) -> Variable:
316-
"""Convert either from Q to wavelength or vice-versa."""
315+
def wavevector_from_wavelength(*, wavelength: Variable, beam: Variable) -> Variable:
316+
r"""Compute a wavevector from a wavelength.
317+
318+
The result is
319+
320+
.. math::
321+
322+
Q = 2 \pi \frac{\hat{b}}{\lambda}
323+
324+
where :math:`\hat{b}` is the normalized beam vector.
325+
326+
Parameters
327+
----------
328+
wavelength:
329+
De Broglie wavelength :math:`\lambda`.
330+
beam:
331+
The direction the neutron travels :math:`\vec{b}`.
332+
It does not have to be normalized.
333+
334+
Returns
335+
-------
336+
:
337+
Wavevector :math:`k`.
338+
Has unit 1/ångström.
339+
"""
340+
c = sc.scalar(2 * np.pi).to(
341+
unit=elem_unit(wavelength) / sc.units.angstrom,
342+
)
343+
return c * (beam / sc.norm(beam)) / wavelength
344+
345+
346+
def _wavelength_elastic_Q_conversions(x: Variable, two_theta: Variable) -> Variable:
347+
"""Convert either from elastic Q to wavelength or vice-versa."""
317348
c = as_float_type(4 * const.pi, x)
318349
return c * sc.sin(as_float_type(two_theta, x) / 2) / x
319350

320351

321-
def Q_from_wavelength(*, wavelength: Variable, two_theta: Variable) -> Variable:
322-
r"""Compute the absolute value of the momentum transfer from wavelength.
352+
def elastic_Q_from_wavelength(*, wavelength: Variable, two_theta: Variable) -> Variable:
353+
r"""Compute the absolute value of the elastic momentum transfer from wavelength.
354+
355+
Attention
356+
---------
357+
:math:`Q` as defined here is the momentum transfer for **elastic** scattering.
323358
324359
The result is
325360
@@ -344,12 +379,16 @@ def Q_from_wavelength(*, wavelength: Variable, two_theta: Variable) -> Variable:
344379
scippneutron.conversions.beamline:
345380
Definition of ``two_theta``.
346381
"""
347-
return _wavelength_Q_conversions(wavelength, two_theta)
382+
return _wavelength_elastic_Q_conversions(wavelength, two_theta)
348383

349384

350385
def wavelength_from_Q(*, Q: Variable, two_theta: Variable) -> Variable:
351386
r"""Compute the wavelength from momentum transfer.
352387
388+
Attention
389+
---------
390+
:math:`Q` as defined here is the momentum transfer for **elastic** scattering.
391+
353392
The result is the de Broglie wavelength
354393
355394
.. math::
@@ -375,14 +414,18 @@ def wavelength_from_Q(*, Q: Variable, two_theta: Variable) -> Variable:
375414
Definition of ``two_theta``.
376415
"""
377416
return sc.to_unit(
378-
_wavelength_Q_conversions(Q, two_theta), unit='angstrom', copy=False
417+
_wavelength_elastic_Q_conversions(Q, two_theta), unit='angstrom', copy=False
379418
)
380419

381420

382-
def Q_elements_from_wavelength(
421+
def elastic_Q_elements_from_wavelength(
383422
*, wavelength: Variable, incident_beam: Variable, scattered_beam: Variable
384423
) -> dict[str, Variable]:
385-
r"""Compute them momentum transfer vector from wavelength.
424+
r"""Compute the elastic momentum transfer vector from wavelength.
425+
426+
Attention
427+
---------
428+
:math:`Q` as defined here is the momentum transfer for **elastic** scattering.
386429
387430
Computes the three components of the Q-vector :math:`Q_x, Q_y, Q_z`
388431
separately using
@@ -499,8 +542,10 @@ def dspacing_from_energy(*, energy: Variable, two_theta: Variable) -> Variable:
499542
return sc.sqrt(c / energy) / sc.sin(as_float_type(two_theta, energy) / 2)
500543

501544

502-
def Q_vec_from_Q_elements(*, Qx: Variable, Qy: Variable, Qz: Variable) -> Variable:
503-
"""Combine elements of Q into a single vector variable.
545+
def elastic_Q_vec_from_Q_elements(
546+
*, Qx: Variable, Qy: Variable, Qz: Variable
547+
) -> Variable:
548+
"""Combine elements of elastic momentum transfer into a single vector variable.
504549
505550
Parameters
506551
----------
@@ -549,10 +594,10 @@ def ub_matrix_from_u_and_b(*, u_matrix: Variable, b_matrix: Variable) -> Variabl
549594
return u_matrix * b_matrix
550595

551596

552-
def hkl_vec_from_Q_vec(
597+
def hkl_vec_from_elastic_Q_vec(
553598
*, Q_vec: Variable, ub_matrix: Variable, sample_rotation: Variable
554599
) -> Variable:
555-
r"""Compute hkl indices from momentum transfer.
600+
r"""Compute hkl indices from elastic momentum transfer.
556601
557602
The hkl indices define the components of the momentum transfer in the
558603
sample coordinate system

tests/conversion/tof_conversions_test.py

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# SPDX-License-Identifier: BSD-3-Clause
2-
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3-
# @author Jan-Lukas Wynen
2+
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
43

54
import numpy as np
65
import pytest
@@ -69,6 +68,13 @@ def time_variables():
6968
)
7069

7170

71+
def wavelength_variables():
72+
return simple_variables(
73+
dims=st.sampled_from(('wavelength', 'lambda', 'tof')),
74+
unit=st.sampled_from(('angstrom', 'mÅ', 'nm')),
75+
)
76+
77+
7278
def space_variables():
7379
return simple_variables(
7480
dims=st.sampled_from(('position', 'spectrum', 'x')),
@@ -146,6 +152,17 @@ def test_wavelength_from_tof_single_precision(Ltotal_dtype):
146152
assert tof_conv.wavelength_from_tof(tof=tof, Ltotal=Ltotal).dtype == 'float32'
147153

148154

155+
@given(wavelength=wavelength_variables(), beam=vector_variables())
156+
@settings(**global_settings)
157+
def test_wavevector_from_wavelength(wavelength: sc.Variable, beam: sc.Variable) -> None:
158+
wavevector = tof_conv.wavevector_from_wavelength(wavelength=wavelength, beam=beam)
159+
naive = sc.to_unit(
160+
2 * np.pi * beam / sc.norm(beam) / wavelength,
161+
'1/angstrom',
162+
)
163+
sc.testing.assert_allclose(wavevector, naive)
164+
165+
149166
@given(
150167
tof=time_variables(),
151168
Ltotal_and_two_theta=n_space_variables(2),
@@ -310,35 +327,39 @@ def test_wavelength_from_energy_single_precision():
310327

311328
@given(wavelength=space_variables(), two_theta=angle_variables())
312329
@settings(**global_settings)
313-
def test_Q_from_wavelength(wavelength, two_theta):
314-
Q = tof_conv.Q_from_wavelength(wavelength=wavelength, two_theta=two_theta)
330+
def test_elastic_Q_from_wavelength(wavelength, two_theta):
331+
Q = tof_conv.elastic_Q_from_wavelength(wavelength=wavelength, two_theta=two_theta)
315332
assert sc.allclose(Q, 4 * np.pi * sc.sin(two_theta / 2) / wavelength)
316333

317334

318335
@pytest.mark.parametrize('wavelength_dtype', ['float64', 'int64'])
319336
@pytest.mark.parametrize('two_theta_dtype', ['float64', 'float32', 'int64'])
320-
def test_Q_from_wavelength_double_precision(wavelength_dtype, two_theta_dtype):
337+
def test_elastic_Q_from_wavelength_double_precision(wavelength_dtype, two_theta_dtype):
321338
wavelength = sc.scalar(3.51, unit='s', dtype=wavelength_dtype)
322339
two_theta = sc.scalar(0.041, unit='deg', dtype=two_theta_dtype)
323340
assert (
324-
tof_conv.Q_from_wavelength(wavelength=wavelength, two_theta=two_theta).dtype
341+
tof_conv.elastic_Q_from_wavelength(
342+
wavelength=wavelength, two_theta=two_theta
343+
).dtype
325344
== 'float64'
326345
)
327346

328347

329348
@pytest.mark.parametrize('two_theta_dtype', ['float64', 'float32', 'int64'])
330-
def test_Q_from_wavelength_single_precision(two_theta_dtype):
349+
def test_elastic_Q_from_wavelength_single_precision(two_theta_dtype):
331350
wavelength = sc.scalar(3.51, unit='s', dtype='float32')
332351
two_theta = sc.scalar(0.041, unit='deg', dtype=two_theta_dtype)
333352
assert (
334-
tof_conv.Q_from_wavelength(wavelength=wavelength, two_theta=two_theta).dtype
353+
tof_conv.elastic_Q_from_wavelength(
354+
wavelength=wavelength, two_theta=two_theta
355+
).dtype
335356
== 'float32'
336357
)
337358

338359

339360
@given(Q=space_variables(), two_theta=angle_variables())
340361
@settings(**global_settings)
341-
def test_wavelength_from_Q(Q, two_theta):
362+
def test_wavelength_from_elastic_Q(Q, two_theta):
342363
Q.unit = f'1/{Q.unit}'
343364
wavelength = tof_conv.wavelength_from_Q(Q=Q, two_theta=two_theta)
344365
assert sc.allclose(
@@ -348,24 +369,24 @@ def test_wavelength_from_Q(Q, two_theta):
348369

349370
@pytest.mark.parametrize('Q_dtype', ['float64', 'int64'])
350371
@pytest.mark.parametrize('two_theta_dtype', ['float64', 'float32', 'int64'])
351-
def test_wavelength_from_Q_double_precision(Q_dtype, two_theta_dtype):
372+
def test_wavelength_from_elastic_Q_double_precision(Q_dtype, two_theta_dtype):
352373
Q = sc.scalar(4.151, unit='1/nm', dtype=Q_dtype)
353374
two_theta = sc.scalar(5.71, unit='deg', dtype=two_theta_dtype)
354375
assert tof_conv.wavelength_from_Q(Q=Q, two_theta=two_theta).dtype == 'float64'
355376

356377

357378
@pytest.mark.parametrize('two_theta_dtype', ['float64', 'float32', 'int64'])
358-
def test_wavelength_from_Q_single_precision(two_theta_dtype):
379+
def test_wavelength_from_elastic_Q_single_precision(two_theta_dtype):
359380
Q = sc.scalar(4.151, unit='1/nm', dtype='float32')
360381
two_theta = sc.scalar(5.71, unit='deg', dtype=two_theta_dtype)
361382
assert tof_conv.wavelength_from_Q(Q=Q, two_theta=two_theta).dtype == 'float32'
362383

363384

364385
@given(incident_beam=vector_variables(), wavelength=space_variables())
365386
@settings(**global_settings)
366-
def test_Q_elements_from_wavelength_equal_beams(incident_beam, wavelength):
387+
def test_elastic_Q_elements_from_wavelength_equal_beams(incident_beam, wavelength):
367388
scattered_beam = incident_beam.copy()
368-
Q_elements = tof_conv.Q_elements_from_wavelength(
389+
Q_elements = tof_conv.elastic_Q_elements_from_wavelength(
369390
wavelength=wavelength,
370391
incident_beam=incident_beam,
371392
scattered_beam=scattered_beam,
@@ -382,18 +403,18 @@ def test_Q_elements_from_wavelength_equal_beams(incident_beam, wavelength):
382403
wavelength=space_variables(),
383404
)
384405
@settings(**global_settings)
385-
def test_Q_elements_from_wavelength_consistent_with_Q(
406+
def test_elastic_Q_elements_from_wavelength_consistent_with_Q(
386407
incident_beam, scattered_beam, wavelength
387408
):
388409
wavelength = wavelength.rename({wavelength.dim: 'wavelength'})
389-
Q_elements = tof_conv.Q_elements_from_wavelength(
410+
Q_elements = tof_conv.elastic_Q_elements_from_wavelength(
390411
wavelength=wavelength,
391412
incident_beam=incident_beam,
392413
scattered_beam=scattered_beam,
393414
)
394415
Qx, Qy, Qz = Q_elements['Qx'], Q_elements['Qy'], Q_elements['Qz']
395416
Q_vec = sc.spatial.as_vectors(Qx, Qy, Qz)
396-
Q = tof_conv.Q_from_wavelength(
417+
Q = tof_conv.elastic_Q_from_wavelength(
397418
wavelength=wavelength,
398419
two_theta=beamline_conv.two_theta(
399420
incident_beam=incident_beam, scattered_beam=scattered_beam
@@ -402,15 +423,15 @@ def test_Q_elements_from_wavelength_consistent_with_Q(
402423
assert sc.allclose(sc.norm(Q_vec), Q)
403424

404425

405-
def test_Q_elements_from_wavelength():
426+
def test_elastic_Q_elements_from_wavelength():
406427
incident_beam = sc.vector([0.0, 0.0, 10.0], unit='m')
407428
scattered_beam = sc.vectors(
408429
dims=['pos'],
409430
values=[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 1.0, 2.0]],
410431
unit='cm',
411432
)
412433
wavelength = sc.array(dims=['wavelength'], values=[0.3, 2.0], unit='angstrom')
413-
Q_elements = tof_conv.Q_elements_from_wavelength(
434+
Q_elements = tof_conv.elastic_Q_elements_from_wavelength(
414435
wavelength=wavelength,
415436
incident_beam=incident_beam,
416437
scattered_beam=scattered_beam,
@@ -502,41 +523,41 @@ def test_dspacing_from_energy_single_precision(two_theta_dtype):
502523
)
503524

504525

505-
def test_q_vec_from_q_elements():
526+
def test_elastic_q_vec_from_q_elements():
506527
Qx, Qy, Qz = (
507528
sc.array(dims=['Q'], values=[2.1 * i, i / 3], unit='1/angstrom')
508529
for i in range(3)
509530
)
510-
q_vec = tof_conv.Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
531+
q_vec = tof_conv.elastic_Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
511532
sc.testing.assert_identical(q_vec.fields.x, Qx)
512533
sc.testing.assert_identical(q_vec.fields.y, Qy)
513534
sc.testing.assert_identical(q_vec.fields.z, Qz)
514535

515536

516-
def test_q_vec_from_q_elements_raises_for_shape_mismatch():
537+
def test_elastic_q_vec_from_q_elements_raises_for_shape_mismatch():
517538
Qx = sc.array(dims=['q'], values=[2.0, 3.0])
518539
Qy = Qx.copy()
519540
Qz = sc.array(dims=['q'], values=[2.0, 3.0, 4.0])
520541
with pytest.raises(sc.DimensionError):
521-
tof_conv.Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
542+
tof_conv.elastic_Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
522543

523544

524-
def test_q_vec_from_q_elements_raises_for_dim_mismatch():
545+
def test_elastic_q_vec_from_q_elements_raises_for_dim_mismatch():
525546
Qx = sc.array(dims=['q'], values=[2.0, 3.0])
526547
Qy = Qx.copy()
527548
Qz = sc.array(dims=['Q'], values=[2.0, 3.0])
528549
with pytest.raises(sc.DimensionError):
529-
tof_conv.Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
550+
tof_conv.elastic_Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
530551

531552

532-
def test_q_vec_from_q_elements_raises_for_unit_mismatch():
553+
def test_elastic_q_vec_from_q_elements_raises_for_unit_mismatch():
533554
Qx, Qy, Qz = (
534555
sc.array(dims=['Q'], values=[2.1 * i, i / 3], unit='1/angstrom')
535556
for i in range(3)
536557
)
537558
Qy.unit = '1/m'
538559
with pytest.raises(sc.UnitError):
539-
tof_conv.Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
560+
tof_conv.elastic_Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
540561

541562

542563
def make_b_matrix() -> sc.Variable:
@@ -576,9 +597,9 @@ def test_ub_matrix_from_u_and_b():
576597

577598

578599
@given(inv_q=n_space_variables(3))
579-
def test_hkl_vec_from_Q_vec(inv_q):
600+
def test_hkl_vec_from_elastic_Q_vec(inv_q):
580601
Qx, Qy, Qz = (sc.reciprocal(x.to(dtype='float64')) for x in inv_q)
581-
Q_vec = tof_conv.Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
602+
Q_vec = tof_conv.elastic_Q_vec_from_Q_elements(Qx=Qx, Qy=Qy, Qz=Qz)
582603

583604
b_matrix = make_b_matrix()
584605
u_coeffs = np.array([0.5, 0.1, -0.4, 0.9])
@@ -590,7 +611,7 @@ def test_hkl_vec_from_Q_vec(inv_q):
590611
value=sample_rotation_coeffs / np.linalg.norm(sample_rotation_coeffs)
591612
)
592613

593-
hkl_vec = tof_conv.hkl_vec_from_Q_vec(
614+
hkl_vec = tof_conv.hkl_vec_from_elastic_Q_vec(
594615
Q_vec=Q_vec, ub_matrix=ub_matrix, sample_rotation=sample_rotation_matrix
595616
)
596617

0 commit comments

Comments
 (0)