Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: ['3.9', '3.10', '3.11']
python-version: ['3.9', '3.10', '3.11', '3.12']
os: [ubuntu-latest, macos-latest, windows-latest]

runs-on: ${{ matrix.os }}
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Development Status :: 3 - Alpha"
]
requires-python = ">=3.9,<3.12"
requires-python = ">=3.9,<3.13"
dependencies = [
"easyscience>=1.0.1",
'easyscience>=1.1.0',
"scipp>=23.12.0",
"refnx>=0.1.15",
"refl1d>=0.8.14",
Expand Down
2 changes: 1 addition & 1 deletion src/easyreflectometry/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.1.0'
__version__ = '1.1.1'
5 changes: 3 additions & 2 deletions src/easyreflectometry/experiment/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import copy
from numbers import Number
from typing import Optional
from typing import Union

import numpy as np
Expand Down Expand Up @@ -189,7 +190,7 @@ def __repr__(self) -> str:
"""String representation of the layer."""
return yaml_dump(self._dict_repr)

def as_dict(self, skip: list = None) -> dict:
def as_dict(self, skip: Optional[list[str]] = None) -> dict:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

Expand All @@ -200,7 +201,7 @@ def as_dict(self, skip: list = None) -> dict:
skip.extend(['sample', 'resolution_function', 'interface'])
this_dict = super().as_dict(skip=skip)
this_dict['sample'] = self.sample.as_dict(skip=skip)
this_dict['resolution_function'] = self.resolution_function.as_dict()
this_dict['resolution_function'] = self.resolution_function.as_dict(skip=skip)
if self.interface is None:
this_dict['interface'] = None
else:
Expand Down
6 changes: 3 additions & 3 deletions src/easyreflectometry/experiment/model_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
__author__ = 'github.com/arm61'

from typing import List
from typing import Optional
from typing import Tuple

from easyreflectometry.sample.base_element_collection import SIZE_DEFAULT_COLLECTION
from easyreflectometry.sample.base_element_collection import BaseElementCollection
Expand All @@ -17,7 +17,7 @@ class ModelCollection(BaseElementCollection):

def __init__(
self,
*models: Optional[tuple[Model]],
*models: Tuple[Model],
name: str = 'EasyModels',
interface=None,
populate_if_none: bool = True,
Expand Down Expand Up @@ -52,7 +52,7 @@ def remove_model(self, idx: int):
del self[idx]

def as_dict(self, skip: List[str] | None = None) -> dict:
this_dict = super().as_dict(skip)
this_dict = super().as_dict(skip=skip)
this_dict['populate_if_none'] = self.populate_if_none
return this_dict

Expand Down
18 changes: 12 additions & 6 deletions src/easyreflectometry/experiment/resolution_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from __future__ import annotations

from abc import abstractmethod
from typing import List
from typing import Optional
from typing import Union

import numpy as np
Expand All @@ -17,10 +19,10 @@

class ResolutionFunction:
@abstractmethod
def smearing(q: Union[np.array, float]) -> np.array: ...
def smearing(self, q: Union[np.array, float]) -> np.array: ...

@abstractmethod
def as_dict() -> dict: ...
def as_dict(self, skip: Optional[List[str]] = None) -> dict: ...

@classmethod
def from_dict(cls, data: dict) -> ResolutionFunction:
Expand All @@ -31,7 +33,7 @@ def from_dict(cls, data: dict) -> ResolutionFunction:
raise ValueError('Unknown resolution function type')


class PercentageFhwm:
class PercentageFhwm(ResolutionFunction):
def __init__(self, constant: Union[None, float] = None):
if constant is None:
constant = DEFAULT_RESOLUTION_FWHM_PERCENTAGE
Expand All @@ -40,17 +42,21 @@ def __init__(self, constant: Union[None, float] = None):
def smearing(self, q: Union[np.array, float]) -> np.array:
return np.ones(np.array(q).size) * self.constant

def as_dict(self) -> dict:
def as_dict(
self, skip: Optional[List[str]] = None
) -> dict[str, str]: # skip is kept for consistency of the as_dict signature
return {'smearing': 'PercentageFhwm', 'constant': self.constant}


class LinearSpline:
class LinearSpline(ResolutionFunction):
def __init__(self, q_data_points: np.array, fwhm_values: np.array):
self.q_data_points = q_data_points
self.fwhm_values = fwhm_values

def smearing(self, q: Union[np.array, float]) -> np.array:
return np.interp(q, self.q_data_points, self.fwhm_values)

def as_dict(self) -> dict:
def as_dict(
self, skip: Optional[List[str]] = None
) -> dict[str, str]: # skip is kept for consistency of the as_dict signature
return {'smearing': 'LinearSpline', 'q_data_points': self.q_data_points, 'fwhm_values': self.fwhm_values}
2 changes: 1 addition & 1 deletion src/easyreflectometry/sample/assemblies/base_assembly.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Any
from typing import Optional

from easyscience.fitting.Constraints import ObjConstraint
from easyscience.Constraints import ObjConstraint

from ..base_core import BaseCore
from ..elements.layers.layer import Layer
Expand Down
2 changes: 1 addition & 1 deletion src/easyreflectometry/sample/assemblies/gradient_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def _dict_repr(self) -> dict[str, str]:
'front_layer': self.front_layer._dict_repr,
}

def as_dict(self, skip: list = None) -> dict:
def as_dict(self, skip: Optional[list[str]] = None) -> dict:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

Expand Down
4 changes: 2 additions & 2 deletions src/easyreflectometry/sample/assemblies/surfactant_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Optional

from easyscience.fitting.Constraints import ObjConstraint
from easyscience.Constraints import ObjConstraint
from easyscience.Objects.new_variable import Parameter

from ..elements.layers.layer_area_per_molecule import LayerAreaPerMolecule
Expand Down Expand Up @@ -230,7 +230,7 @@ def _dict_repr(self) -> dict:
}
}

def as_dict(self, skip: list = None) -> dict:
def as_dict(self, skip: Optional[list[str]] = None) -> dict:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import numpy as np
from easyscience import global_object
from easyscience.fitting.Constraints import FunctionalConstraint
from easyscience.Constraints import FunctionalConstraint
from easyscience.Objects.new_variable import Parameter

from easyreflectometry.parameter_utils import get_as_parameter
Expand Down Expand Up @@ -262,16 +262,16 @@ def _dict_repr(self) -> dict[str, str]:
dict_repr['area_per_molecule'] = f'{self.area_per_molecule:.2f} ' f'{self._area_per_molecule.unit}'
return dict_repr

def as_dict(self, skip: list = None) -> dict[str, str]:
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

:param skip: List of keys to skip, defaults to `None`.
"""
this_dict = super().as_dict(skip=skip)
this_dict['solvent_fraction'] = self.material._fraction.as_dict()
this_dict['area_per_molecule'] = self._area_per_molecule.as_dict()
this_dict['solvent'] = self.solvent.as_dict()
this_dict['solvent_fraction'] = self.material._fraction.as_dict(skip=skip)
this_dict['area_per_molecule'] = self._area_per_molecule.as_dict(skip=skip)
this_dict['solvent'] = self.solvent.as_dict(skip=skip)
del this_dict['material']
del this_dict['_scattering_length_real']
del this_dict['_scattering_length_imag']
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
__author__ = 'github.com/arm61'
from typing import Tuple
from typing import Union

from ...base_element_collection import SIZE_DEFAULT_COLLECTION
Expand All @@ -13,13 +14,21 @@ class MaterialCollection(BaseElementCollection):

def __init__(
self,
*materials: Union[list[Union[Material, MaterialMixture]], None],
*materials: Tuple[Union[Material, MaterialMixture]],
name: str = 'EasyMaterials',
interface=None,
populate_if_none: bool = True,
**kwargs,
):
if not materials:
materials = [Material(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)]
if not materials: # Empty tuple if no materials are provided
if populate_if_none:
materials = [Material(interface=interface) for _ in range(SIZE_DEFAULT_COLLECTION)]
else:
materials = []
# Needed to ensure an empty list is created when saving and instatiating the object as_dict -> from_dict
# Else collisions might occur in global_object.map
self.populate_if_none = False

super().__init__(
name,
interface,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import numpy as np
from easyscience import global_object
from easyscience.fitting.Constraints import FunctionalConstraint
from easyscience.Constraints import FunctionalConstraint
from easyscience.Objects.new_variable import Parameter

from easyreflectometry.parameter_utils import get_as_parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Union

from easyscience import global_object
from easyscience.fitting.Constraints import FunctionalConstraint
from easyscience.Constraints import FunctionalConstraint
from easyscience.Objects.new_variable import Parameter

from easyreflectometry.parameter_utils import get_as_parameter
Expand Down Expand Up @@ -201,14 +201,14 @@ def _dict_repr(self) -> dict[str, str]:
}
}

def as_dict(self, skip: list = None) -> dict[str, str]:
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

:param skip: List of keys to skip, defaults to `None`.
"""
this_dict = super().as_dict(skip=skip)
this_dict['material_a'] = self._material_a.as_dict()
this_dict['material_b'] = self._material_b.as_dict()
this_dict['fraction'] = self._fraction.as_dict()
this_dict['material_a'] = self._material_a.as_dict(skip=skip)
this_dict['material_b'] = self._material_b.as_dict(skip=skip)
this_dict['fraction'] = self._fraction.as_dict(skip=skip)
return this_dict
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,16 @@ def _dict_repr(self) -> dict[str, str]:
}
}

def as_dict(self, skip: list = None) -> dict[str, str]:
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__

:param skip: List of keys to skip, defaults to `None`.
"""
this_dict = super().as_dict(skip=skip)
this_dict['material'] = self.material.as_dict()
this_dict['solvent'] = self.solvent.as_dict()
this_dict['solvent_fraction'] = self._fraction.as_dict()
this_dict['material'] = self.material.as_dict(skip=skip)
this_dict['solvent'] = self.solvent.as_dict(skip=skip)
this_dict['solvent_fraction'] = self._fraction.as_dict(skip=skip)
# Property and protected varible from material_mixture
del this_dict['material_a']
del this_dict['_material_a']
Expand Down
2 changes: 1 addition & 1 deletion tests/experiment/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def test_repr_resolution_function(self):
'interface',
[None, CalculatorFactory()],
)
def test_dict_round_trip(interface): # , additional_layer):
def test_dict_round_trip(interface):
# When
resolution_function = LinearSpline([0, 10], [0, 10])
model = Model(interface=interface)
Expand Down
56 changes: 56 additions & 0 deletions tests/test_topmost_nesting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
Tests exercising the methods of the topmost classes for nested structure.
To ensure that the parameters are relayed.
"""

from copy import copy

from numpy.testing import assert_almost_equal

from easyreflectometry.calculators import CalculatorFactory
from easyreflectometry.experiment import LinearSpline
from easyreflectometry.experiment import Model
from easyreflectometry.sample import Multilayer
from easyreflectometry.sample import RepeatingMultilayer
from easyreflectometry.sample import SurfactantLayer


def test_dict_skip_unique_name():
# When
resolution_function = LinearSpline([0, 10], [0, 10])
model = Model(interface=CalculatorFactory())
model.resolution_function = resolution_function
for additional_layer in [SurfactantLayer(), Multilayer(), RepeatingMultilayer()]:
model.add_item(additional_layer)

# Then
dict_no_unique_name = model.as_dict(skip=['unique_name'])

# Expect
assert 'unique_name' not in dict_no_unique_name


def test_copy():
# When
resolution_function = LinearSpline([0, 10], [0, 10])
model = Model(interface=CalculatorFactory())
model.resolution_function = resolution_function
for additional_layer in [SurfactantLayer(), Multilayer(), RepeatingMultilayer()]:
model.add_item(additional_layer)

# Then
model_copy = copy(model)

# Expect
assert sorted(model.as_data_dict()) == sorted(model_copy.as_data_dict())
assert model._resolution_function.smearing(5.5) == model_copy._resolution_function.smearing(5.5)
assert model.interface().name == model_copy.interface().name
assert_almost_equal(
model.interface().fit_func([0.3], model.unique_name),
model_copy.interface().fit_func([0.3], model_copy.unique_name),
)
assert model.unique_name != model_copy.unique_name
assert model.name == model_copy.name
assert model.as_data_dict(skip=['interface', 'unique_name', 'resolution_function']) == model_copy.as_data_dict(
skip=['interface', 'unique_name', 'resolution_function']
)