Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def create_component_collections(
self._component_collections = []
return self._component_collections

Q = Q.values
component_collection_list = [None] * len(Q)
# In more complex models, this is used to scale the area of the
# Lorentzians and the delta function.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ def create_component_collections(
Q = self.Q
if Q is None:
return []
Q = Q.values

if self._allow_Q_variation['A_0'] is True:
A_0_list, A_1_list = self._create_A0_A1_parameter_lists(self.A_0)
Expand Down Expand Up @@ -812,7 +813,7 @@ def _create_A0_A1_parameter_lists(
"""
A_0_list = []
A_1_list = []
for _ in self.Q:
for _ in range(len(self.Q)):
a0 = Parameter(
name='A_0',
value=float(A_0.value),
Expand Down Expand Up @@ -858,7 +859,7 @@ def _create_lorentzian_width_parameter_list(
min=MINIMUM_WIDTH,
unit=self.unit,
)
for _ in self.Q
for _ in range(len(self.Q))
]

# ------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,14 @@ def scale(self, scale: Numeric) -> None:
self._scale.value = float(scale)

@property
def Q(self) -> np.ndarray | None:
def Q(self) -> sc.Variable | None:
"""
Get the Q values of the SampleModel.

Returns
-------
np.ndarray | None
The Q values of the SampleModel, or None if not set.
sc.Variable | None
The Q values of the SampleModel in 1/angstrom, or None if not set.
"""
return self._Q

Expand Down Expand Up @@ -185,7 +185,7 @@ def Q(self, value: Q_type | None) -> None:
self._on_Q_change()
return

if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
if len(old_Q) != len(new_Q) or not sc.allclose(old_Q, new_Q):
raise ValueError(
'New Q values are not similar to the old ones. '
'To change Q values, first run clear_Q().'
Expand Down Expand Up @@ -483,7 +483,7 @@ def _ensure_Q(self, Q: Q_type) -> np.ndarray:
if Q is None:
raise ValueError('Q must be provided either as an argument or set in the model.')

return _validate_and_convert_Q(Q)
return _validate_and_convert_Q(Q).values

# ------------------------------------------------------------------
# dunder methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ def create_component_collections(
self._component_collections = []
return self._component_collections

Q = Q.values
component_collection_list = [None] * len(Q)
# In more complex models, this is used to scale the area of the
# Lorentzians and the delta function.
Expand Down
11 changes: 5 additions & 6 deletions src/easydynamics/sample_model/instrument_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

from copy import copy

import numpy as np
import scipp as sc
from easyscience.base_classes.new_base import NewBase
from easyscience.variable import Parameter
Expand Down Expand Up @@ -217,14 +216,14 @@ def background_model(self, value: BackgroundModel) -> None:
self._on_background_model_change()

@property
def Q(self) -> np.ndarray | None:
def Q(self) -> sc.Variable | None:
"""
Get the Q values of the InstrumentModel.

Returns
-------
np.ndarray | None
The Q values of the InstrumentModel, or None if not set.
sc.Variable | None
The Q values of the InstrumentModel in 1/angstrom, or None if not set.
"""
return self._Q

Expand Down Expand Up @@ -257,7 +256,7 @@ def Q(self, value: Q_type | None) -> None:
self._on_Q_change()
return

if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
if len(old_Q) != len(new_Q) or not sc.allclose(old_Q, new_Q):
raise ValueError(
'New Q values are not similar to the old ones. '
'To change Q values, first run clear_Q().'
Expand Down Expand Up @@ -532,7 +531,7 @@ def _generate_energy_offsets(self) -> None:
self._energy_offsets = []
return

self._energy_offsets = [copy(self._energy_offset) for _ in self._Q]
self._energy_offsets = [copy(self._energy_offset) for _ in range(len(self._Q))]

def _on_Q_change(self) -> None:
"""Handle changes to the Q values."""
Expand Down
12 changes: 6 additions & 6 deletions src/easydynamics/sample_model/model_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,14 @@ def component_collections_is_dirty(self) -> bool:
return self._component_collections_is_dirty

@property
def Q(self) -> np.ndarray | None:
def Q(self) -> sc.Variable | None:
"""
Get the Q values of the SampleModel.

Returns
-------
np.ndarray | None
The Q values of the SampleModel, or None if not set.
sc.Variable | None
The Q values of the SampleModel in 1/angstrom, or None if not set.
"""
return self._Q

Expand Down Expand Up @@ -226,7 +226,7 @@ def Q(self, value: Q_type | None) -> None:
self._on_Q_change()
return

if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
if len(old_Q) != len(new_Q) or not sc.allclose(old_Q, new_Q):
raise ValueError(
'New Q values are not similar to the old ones. '
'To change Q values, first run clear_Q().'
Expand Down Expand Up @@ -377,7 +377,7 @@ def _generate_component_collections(self) -> None:
return

self._component_collections = []
for _ in self.Q:
for _ in range(len(self.Q)):
self._component_collections.append(copy(self._components))

def _on_Q_change(self) -> None:
Expand Down Expand Up @@ -405,6 +405,6 @@ def __repr__(self) -> str:
f'{self.__class__.__name__}('
f'unique_name={self.unique_name!r}, '
f'unit={self.unit}, '
f'Q={self.Q}, '
f'Q={None if self.Q is None else self.Q.values}, '
f'components={self.components})'
)
19 changes: 11 additions & 8 deletions src/easydynamics/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,17 @@ def energy_to_scipp(energy: np.ndarray, unit: str | sc.Unit) -> sc.Variable:

def _validate_and_convert_Q(
Q: np.ndarray | Numeric | list | ArrayLike | sc.Variable | None,
) -> np.ndarray | None:
) -> sc.Variable | None:
"""
Validate and convert Q to a numpy array.
Validate and convert Q to a scipp Variable in 1/angstrom.

Numbers, lists, and numpy arrays are assumed to be in 1/angstrom. Scipp Variables may be in any
unit convertible to 1/angstrom and are converted.

Parameters
----------
Q : np.ndarray | Numeric | list | ArrayLike | sc.Variable | None
Scattering vector values in 1/angstrom.
Scattering vector values.

Raises
------
Expand All @@ -183,8 +186,8 @@ def _validate_and_convert_Q(

Returns
-------
np.ndarray | None
Q as a np.ndarray or None if Q is None.
sc.Variable | None
Q as a sc.Variable with dimension 'Q' and unit 1/angstrom, or None if Q is None.
"""
if Q is None:
return None
Expand All @@ -199,13 +202,13 @@ def _validate_and_convert_Q(
if Q.ndim > 1:
raise ValueError('Q must be a 1-dimensional array.')

Q = sc.array(dims=['Q'], values=Q, unit='1/angstrom')
Q = sc.array(dims=['Q'], values=Q, unit=CANONICAL_Q_UNIT)

if isinstance(Q, sc.Variable):
if Q.dims != ('Q',):
raise ValueError("Q must have a single dimension named 'Q'.")
Q = Q.to(unit='1/angstrom')
return Q.values
Q = Q.to(unit=CANONICAL_Q_UNIT)
return Q


def _validate_unit(unit: str | sc.Unit | None) -> sc.Unit | None:
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/easydynamics/analysis/test_analysis_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,8 @@ def test_on_experiment_changed_updates_Q(self, analysis_base):
# EXPECT
# assert that the Q attribute was set
np.testing.assert_array_equal(analysis_base.Q, fake_Q)
np.testing.assert_array_equal(analysis_base.sample_model.Q, fake_Q)
np.testing.assert_array_equal(analysis_base.instrument_model.Q, fake_Q)
np.testing.assert_array_equal(analysis_base.sample_model.Q.values, fake_Q)
np.testing.assert_array_equal(analysis_base.instrument_model.Q.values, fake_Q)

def test_on_sample_model_changed_updates_Q(self, analysis_base):
# WHEN
Expand All @@ -518,7 +518,7 @@ def test_on_sample_model_changed_updates_Q(self, analysis_base):
analysis_base._on_sample_model_changed()

# EXPECT
np.testing.assert_array_equal(analysis_base.sample_model.Q, fake_Q)
np.testing.assert_array_equal(analysis_base.sample_model.Q.values, fake_Q)

def test_on_instrument_model_changed_updates_Q(self, analysis_base):
fake_Q = [1, 2, 3]
Expand All @@ -530,7 +530,7 @@ def test_on_instrument_model_changed_updates_Q(self, analysis_base):
mock_Q.return_value = fake_Q

analysis_base._on_instrument_model_changed()
np.testing.assert_array_equal(analysis_base.instrument_model.Q, fake_Q)
np.testing.assert_array_equal(analysis_base.instrument_model.Q.values, fake_Q)

def test_repr(self, analysis_base):
# WHEN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ def test_calculate_EISF_with_Q(self, delta_lorentz_model_with_Q):
for i in range(len(eisf)):
expected = delta_lorentz_model_with_Q._A_0_list[i].value * np.exp(
-delta_lorentz_model_with_Q.mean_u_squared.value
* delta_lorentz_model_with_Q.Q[i] ** 2
* delta_lorentz_model_with_Q.Q.values[i] ** 2
)
assert eisf[i] == pytest.approx(expected)

Expand All @@ -428,7 +428,7 @@ def test_calculate_QISF_with_Q(self, delta_lorentz_model_with_Q):
for i in range(len(qisf)):
expected = delta_lorentz_model_with_Q._A_1_list[i].value * np.exp(
-delta_lorentz_model_with_Q.mean_u_squared.value
* delta_lorentz_model_with_Q.Q[i] ** 2
* delta_lorentz_model_with_Q.Q.values[i] ** 2
)

assert qisf[i] == pytest.approx(expected)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import numpy as np
import pytest
import scipp as sc
from easyscience.variable.parameter import Parameter

from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase
Expand Down Expand Up @@ -131,7 +132,9 @@ def test_Q_property(self, diffusion_model):
diffusion_model.Q = [1.0, 2.0, 3.0]

# EXPECT
np.testing.assert_allclose(diffusion_model.Q, [1.0, 2.0, 3.0])
assert isinstance(diffusion_model.Q, sc.Variable)
assert diffusion_model.Q.unit == sc.Unit('1/angstrom')
np.testing.assert_allclose(diffusion_model.Q.values, [1.0, 2.0, 3.0])

# THEN EXPECT
with pytest.raises(ValueError, match=r'New Q values are not similar to the old ones'):
Expand All @@ -147,6 +150,16 @@ def test_Q_property(self, diffusion_model):
# EXPECT
assert diffusion_model.Q is None

def test_Q_setter_accepts_equivalent_scipp_Q_in_other_unit(self, diffusion_model):
# WHEN
diffusion_model.Q = [1.0, 2.0, 3.0]

# THEN: the same Q in 1/nm is equivalent after conversion, so no error is raised
diffusion_model.Q = sc.Variable(dims=['Q'], values=[10.0, 20.0, 30.0], unit='1/nm')

# EXPECT
np.testing.assert_allclose(diffusion_model.Q.values, [1.0, 2.0, 3.0])

def test_repr(self, diffusion_model):
# WHEN THEN
repr_str = repr(diffusion_model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_init(self, background_model):
assert model.display_name == 'InitModel'
assert model.unit == 'meV'
assert len(model.components) == 2
np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.Q.values, np.array([1.0, 2.0, 3.0]))

@pytest.mark.parametrize(
'invalid_component, expected_error_msg',
Expand Down
32 changes: 22 additions & 10 deletions tests/unit/easydynamics/sample_model/test_instrument_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@ def test_init(self, instrument_model):

# EXPECT
assert model.display_name == 'TestInstrumentModel'
np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.Q.values, np.array([1.0, 2.0, 3.0]))
assert isinstance(model.background_model, BackgroundModel)
assert isinstance(model.resolution_model, ResolutionModel)
np.testing.assert_array_equal(model.background_model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.resolution_model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.background_model.Q.values, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.resolution_model.Q.values, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.Q.values, np.array([1.0, 2.0, 3.0]))

def test_init_defaults(self):
# WHEN THEN
Expand Down Expand Up @@ -441,8 +441,12 @@ def test_Q_setter(self, instrument_model_without_Q):

# EXPECT
assert instrument_model_without_Q._energy_offsets_is_dirty is True
np.testing.assert_array_equal(instrument_model_without_Q.background_model.Q, first_new_Q)
np.testing.assert_array_equal(instrument_model_without_Q.resolution_model.Q, first_new_Q)
np.testing.assert_array_equal(
instrument_model_without_Q.background_model.Q.values, first_new_Q
)
np.testing.assert_array_equal(
instrument_model_without_Q.resolution_model.Q.values, first_new_Q
)

# THEN
new_Q = np.array([4.0, 5.0, 6.0])
Expand All @@ -457,17 +461,25 @@ def test_Q_setter(self, instrument_model_without_Q):

# EXPECT
# Q values remain unchanged
np.testing.assert_array_equal(instrument_model_without_Q.background_model.Q, first_new_Q)
np.testing.assert_array_equal(instrument_model_without_Q.resolution_model.Q, first_new_Q)
np.testing.assert_array_equal(
instrument_model_without_Q.background_model.Q.values, first_new_Q
)
np.testing.assert_array_equal(
instrument_model_without_Q.resolution_model.Q.values, first_new_Q
)

# THEN - set Q to an equivalent scipp Variable; values match so should be accepted
new_Q = sc.Variable(dims=['Q'], values=[1.0, 2.0, 3.0], unit='1/angstrom')
instrument_model_without_Q.Q = new_Q

# EXPECT - Q propagated to child models, offsets marked dirty again
assert instrument_model_without_Q._energy_offsets_is_dirty is True
np.testing.assert_array_equal(instrument_model_without_Q.background_model.Q, first_new_Q)
np.testing.assert_array_equal(instrument_model_without_Q.resolution_model.Q, first_new_Q)
np.testing.assert_array_equal(
instrument_model_without_Q.background_model.Q.values, first_new_Q
)
np.testing.assert_array_equal(
instrument_model_without_Q.resolution_model.Q.values, first_new_Q
)

def test_fix_and_free_offset(self, instrument_model):
# WHEN
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/easydynamics/sample_model/test_model_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_init(self, model_base):
assert model.display_name == 'InitModel'
assert model.unit == 'meV'
assert len(model.components) == 2
np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0]))
np.testing.assert_array_equal(model.Q.values, np.array([1.0, 2.0, 3.0]))

def test_init_raises_with_invalid_components(self):
# WHEN / THEN / EXPECT
Expand Down Expand Up @@ -352,7 +352,7 @@ def test_Q_setter_when_current_Q_is_none(self, model_base):
model_base.Q = new_Q

# EXPECT
np.testing.assert_array_equal(model_base.Q, np.array(new_Q))
np.testing.assert_array_equal(model_base.Q.values, np.array(new_Q))

def test_clear_Q(self, model_base):
# WHEN
Expand Down
Loading
Loading