Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 docs/docs/tutorials/sample_model.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
"version": "3.14.5"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/tutorials/tutorial0_more_advanced.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
"version": "3.14.5"
}
},
"nbformat": 4,
Expand Down
47 changes: 13 additions & 34 deletions docs/docs/tutorials/tutorial1_brownian.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -94,32 +94,15 @@
"id": "6c87b01c",
"metadata": {},
"source": [
"We now want to fit the vanadium data to determine our resolution. The scattering from vanadium is almost exclusively incoherent elastic, so we model it as a delta function. We do this by creating a `SampleModel` and adding a `DeltaFunction` component to it. The component acts as a template and gets copied to every `Q` when we attach the `SampleModel` to our `Analysis` object. Let's create the `SampleModel`.\n",
"\n",
"We do not give the `DeltaFunction` a `center` value. In this case, the center will be fixed at 0 energy transfer. We set the start value of the area to 1."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6762faba",
"metadata": {},
"outputs": [],
"source": [
"delta_function = sm.DeltaFunction(name='DeltaFunction', area=1)\n",
"sample_model = sm.SampleModel(components=delta_function)"
"We now want to fit the vanadium data to determine our resolution. The scattering from vanadium is almost exclusively incoherent elastic, so it can be modelled as a delta function. There are two ways to do this. The first is to add a `DeltaFunction` component to a `SampleModel` and next create a `ResolutionModel`. The second method, which we here shall use, is to fit the data to a `SampleModel` and then convert it to a `ResolutionModel`."
]
},
{
"cell_type": "markdown",
"id": "dc82774e",
"metadata": {},
"source": [
"We now want to define our resolution function. We will here model it as a Gaussian. We create a `ComponentCollection` and append the `Gaussian` to it. We can add as many components to our resolution as we like; sometimes you need several Gaussians and other functions to accurately describe the resolution.\n",
"\n",
"We fix the area of the resolution to have value 1. If we did not do this, we would fit both the area of the delta function and of the resolution Gaussian, and the fit would never converge.\n",
"\n",
"We finally insert the components in a `ResolutionModel`"
"We now want to define our resolution function. We will here model it as a Gaussian. We create a `ComponentCollection` and append the `Gaussian` to it. We can add as many components to our resolution as we like; sometimes you need several Gaussians and other functions to accurately describe the resolution."
]
},
{
Expand All @@ -129,19 +112,18 @@
"metadata": {},
"outputs": [],
"source": [
"resolution_components = sm.ComponentCollection()\n",
"vanadium_components = sm.ComponentCollection()\n",
"res_gauss = sm.Gaussian(width=0.1, area=1, name='Res. Gauss')\n",
"res_gauss.area.fixed = True\n",
"resolution_components.append_component(res_gauss)\n",
"resolution_model = sm.ResolutionModel(components=resolution_components)"
"vanadium_components.append_component(res_gauss)\n",
"vanadium_model = sm.SampleModel(components=vanadium_components)"
]
},
{
"cell_type": "markdown",
"id": "088ac17d",
"metadata": {},
"source": [
"The background intensity was not 0, so we also create a background model. We use a `Polynomial` with a single coefficient, i.e. a flat background. We here show how to create the `BackgroundModel` and add the background in a single line. We could of course also add it like we did for the `SampleModel` or first create a `ComponentCollection` like we did for the `ResolutionModel`"
"The background intensity was not 0, so we also create a background model. We use a `Polynomial` with a single coefficient, i.e. a flat background. We here show how to create the `BackgroundModel` and add the background in a single line. We could of course also add it like we did for the `SampleModel` or first create a `ComponentCollection` like we just did for the resolution. "
]
},
{
Expand All @@ -159,7 +141,7 @@
"id": "eae3d14b",
"metadata": {},
"source": [
"We combine the resolution abd background model into an `InstrumentModel`. This model also contains a fittable energy offset to account for instrument misalignment. All components are centered at this energy offset."
"We add the background model to an `InstrumentModel`. This model also contains a fittable energy offset to account for instrument misalignment. All components are centered at this energy offset."
]
},
{
Expand All @@ -170,7 +152,6 @@
"outputs": [],
"source": [
"instrument_model = sm.InstrumentModel(\n",
" resolution_model=resolution_model,\n",
" background_model=background_model,\n",
")"
]
Expand All @@ -193,7 +174,7 @@
"vanadium_analysis = edyn.Analysis(\n",
" display_name='Vanadium Full Analysis',\n",
" experiment=vanadium_experiment,\n",
" sample_model=sample_model,\n",
" sample_model=vanadium_model,\n",
" instrument_model=instrument_model,\n",
")"
]
Expand Down Expand Up @@ -265,7 +246,7 @@
"outputs": [],
"source": [
"# Plot some of fitted parameters as a function of Q\n",
"vanadium_analysis.plot_parameters(names=['DeltaFunction area'])"
"vanadium_analysis.plot_parameters(names=['Res. Gauss area'])"
]
},
{
Expand Down Expand Up @@ -356,7 +337,7 @@
"id": "927b8fb5",
"metadata": {},
"source": [
"We also create a new instrument_model and attach it to our analysis, giving it the resolution model determined in the vanadium analysis. We further fix all parameters in the resolution model and normalize it."
"We also create a new instrument_model and attach it to our analysis. We now give it the sample model from the vanadium fit. All parameters are automatically fixed and the resolution model is normalized to have area 1. "
]
},
{
Expand All @@ -368,10 +349,8 @@
"source": [
"instrument_model = sm.InstrumentModel(\n",
" background_model=background_model,\n",
" resolution_model=vanadium_analysis.instrument_model.resolution_model,\n",
" resolution_model=vanadium_analysis.sample_model,\n",
")\n",
"instrument_model.resolution_model.fix_all_parameters()\n",
"instrument_model.normalize_resolution()\n",
"\n",
"diffusion_analysis = edyn.Analysis(\n",
" display_name='Diffusion Analysis',\n",
Expand Down Expand Up @@ -607,7 +586,7 @@
"source": [
"instrument_model = sm.InstrumentModel(\n",
" background_model=background_model,\n",
" resolution_model=vanadium_analysis.instrument_model.resolution_model,\n",
" resolution_model=vanadium_analysis.sample_model,\n",
")"
]
},
Expand Down Expand Up @@ -726,7 +705,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
"version": "3.14.5"
}
},
"nbformat": 4,
Expand Down
21 changes: 7 additions & 14 deletions docs/docs/tutorials/tutorial2_nanoparticles.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
"source": [
"The resolution function can to a good approximation be modelled as a Gaussian. In truth, it has small Lorentzian tails, but we leave it as an exercise for the interested reader to add this.\n",
"\n",
"We define the `SampleModel` to be a `DeltaFunction`, since there is essentially only elastic scattering present. We also add a constant background using a `Polynomial`. \n",
"We fit the resolution directly as in the previous tutorial, since there is essentially only elastic scattering present. We also add a constant background using a `Polynomial`. \n",
"\n",
"As in Tutorial 1 we place everything in an `Analysis` object and plot the start guesses."
]
Expand All @@ -135,16 +135,12 @@
"metadata": {},
"outputs": [],
"source": [
"delta_function = sm.DeltaFunction(area=100)\n",
"res_sample_model = sm.SampleModel(components=delta_function)\n",
"\n",
"res_resolution_model = sm.ResolutionModel()\n",
"res_sample_model = sm.SampleModel()\n",
"res_components = sm.ComponentCollection()\n",
"res_gauss = sm.Gaussian(area=1, width=0.02)\n",
"res_gauss.area.fixed = True\n",
"res_gauss = sm.Gaussian(area=40, width=0.02)\n",
"\n",
"res_components.append_component(res_gauss)\n",
"res_resolution_model.components = res_components\n",
"res_sample_model.components = res_components\n",
"\n",
"background_model = sm.BackgroundModel()\n",
"polynomial = sm.Polynomial(coefficients=[1.5])\n",
Expand All @@ -153,7 +149,6 @@
"\n",
"\n",
"res_instrument_model = sm.InstrumentModel(\n",
" resolution_model=res_resolution_model,\n",
" background_model=background_model,\n",
")\n",
"\n",
Expand Down Expand Up @@ -267,10 +262,8 @@
"\n",
"instrument_model = sm.InstrumentModel(\n",
" background_model=background_model,\n",
" resolution_model=res_analysis.instrument_model.resolution_model,\n",
" resolution_model=res_analysis.sample_model,\n",
")\n",
"instrument_model.resolution_model.fix_all_parameters()\n",
"instrument_model.normalize_resolution()\n",
"\n",
"\n",
"analysis = edyn.Analysis(\n",
Expand Down Expand Up @@ -407,7 +400,7 @@
"\n",
"instrument_model = sm.InstrumentModel(\n",
" background_model=background_model,\n",
" resolution_model=res_analysis.instrument_model.resolution_model,\n",
" resolution_model=res_analysis.sample_model,\n",
")\n",
"instrument_model.resolution_model.fix_all_parameters()\n",
"instrument_model.normalize_resolution()\n",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are these still needed?

Expand Down Expand Up @@ -616,7 +609,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.14.4"
"version": "3.14.5"
}
},
"nbformat": 4,
Expand Down
29 changes: 18 additions & 11 deletions src/easydynamics/sample_model/instrument_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from easydynamics.sample_model.background_model import BackgroundModel
from easydynamics.sample_model.resolution_model import ResolutionModel
from easydynamics.sample_model.sample_model import SampleModel
from easydynamics.utils.utils import Numeric
from easydynamics.utils.utils import Q_type
from easydynamics.utils.utils import _validate_and_convert_Q
Expand All @@ -29,7 +30,7 @@ def __init__(
display_name: str = 'MyInstrumentModel',
unique_name: str | None = None,
Q: Q_type | None = None,
resolution_model: ResolutionModel | None = None,
resolution_model: ResolutionModel | SampleModel | None = None,
background_model: BackgroundModel | None = None,
energy_offset: Numeric | None = None,
unit: str | sc.Unit = 'meV',
Expand All @@ -45,9 +46,10 @@ def __init__(
The unique name of the InstrumentModel.
Q : Q_type | None, default=None
The Q values where the instrument is modelled.
resolution_model : ResolutionModel | None, default=None
The resolution model of the instrument. If None, an empty resolution model is created
and no resolution convolution is carried out.
resolution_model : ResolutionModel | SampleModel | None, default=None
The resolution model of the instrument. If a SampleModel it will be converted to a
ResolutionModel. If None, an empty resolution model is created and no resolution
convolution is carried out.
background_model : BackgroundModel | None, default=None
The background model of the instrument. If None, an empty background model is created,
and the background evaluates to 0.
Expand All @@ -73,11 +75,13 @@ def __init__(
if resolution_model is None:
self._resolution_model = ResolutionModel()
else:
if not isinstance(resolution_model, ResolutionModel):
if not isinstance(resolution_model, (ResolutionModel, SampleModel)):
raise TypeError(
f'resolution_model must be a ResolutionModel or None, '
f'resolution_model must be a ResolutionModel, a SampleModel or None, '
f'got {type(resolution_model).__name__}'
)
if isinstance(resolution_model, SampleModel):
resolution_model = ResolutionModel.from_sample_model(resolution_model)
self._resolution_model = resolution_model

if background_model is None:
Expand Down Expand Up @@ -122,24 +126,27 @@ def resolution_model(self) -> ResolutionModel:
return self._resolution_model

@resolution_model.setter
def resolution_model(self, value: ResolutionModel) -> None:
def resolution_model(self, value: ResolutionModel | SampleModel) -> None:
"""
Set the resolution model of the instrument.

Parameters
----------
value : ResolutionModel
value : ResolutionModel | SampleModel
The new resolution model of the instrument.

Raises
------
TypeError
If value is not a ResolutionModel.
If value is not a ResolutionModel or SampleModel.
"""
if not isinstance(value, ResolutionModel):
if not isinstance(value, (ResolutionModel, SampleModel)):
raise TypeError(
f'resolution_model must be a ResolutionModel, got {type(value).__name__}'
f'resolution_model must be a ResolutionModel or SampleModel, '
f'got {type(value).__name__}'
)
if isinstance(value, SampleModel):
value = ResolutionModel.from_sample_model(value)
self._resolution_model = value
self._on_resolution_model_change()

Expand Down
67 changes: 66 additions & 1 deletion src/easydynamics/sample_model/resolution_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
from easydynamics.sample_model.component_collection import ComponentCollection
from easydynamics.sample_model.components import DeltaFunction
from easydynamics.sample_model.components import Polynomial
from easydynamics.sample_model.components.exponential import Exponential
from easydynamics.sample_model.components.model_component import ModelComponent
from easydynamics.sample_model.model_base import ModelBase
from easydynamics.sample_model.sample_model import SampleModel
from easydynamics.utils.utils import Q_type


Expand Down Expand Up @@ -70,9 +72,72 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N
components = component if isinstance(component, ComponentCollection) else (component,)

for comp in components:
if isinstance(comp, (DeltaFunction, Polynomial)):
if isinstance(comp, (DeltaFunction, Polynomial, Exponential)):
raise TypeError(
f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
)

super().append_component(component)

@classmethod
def from_sample_model(
cls,
sample_model: SampleModel,
normalize_area: bool = True,
fix_parameters: bool = True,
) -> 'ResolutionModel':
"""
Create a ResolutionModel from a SampleModel.

Parameters
----------
sample_model : SampleModel
SampleModel to create the ResolutionModel from.
normalize_area : bool, default=True
Whether to normalize the components in the ResolutionModel to have area 1.
fix_parameters : bool, default=True
Whether to fix the parameters in the ResolutionModel.

Returns
-------
'ResolutionModel'
ResolutionModel created from the SampleModel.

Raises
------
TypeError
If sample_model is not a SampleModel, or if normalize_area or fix_parameters are not
bool.
"""
if not isinstance(sample_model, SampleModel):
raise TypeError(
f'sample_model must be an instance of SampleModel. Got {type(sample_model)}.'
)

if not isinstance(normalize_area, bool):
raise TypeError('normalize_area must be True or False.')

if not isinstance(fix_parameters, bool):
raise TypeError('fix_parameters must be True or False.')

resolution_model = cls(
display_name=sample_model.display_name,
unit=sample_model.unit,
components=sample_model.components,
Q=sample_model.Q,
)
Comment on lines +125 to +130
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cls(components=sample_model.components, Q=sample_model.Q, ...) invokes ModelBase._generate_component_collections, which calls the base append_component, not ResolutionModel.append_component.
This means the check for DeltaFunction, Polynomial, and Exponential is never done.

If someone passes a SampleModel that contains a DeltaFunction, it will silently find its way to the ResolutionModel. Consider adding an explicit check at the top of from_sample_model:

for comp in sample_model.components:
    if isinstance(comp, (DeltaFunction, Polynomial, Exponential)):
        raise TypeError(
            f'SampleModel contains a {comp.__class__.__name__}, '
            f'which is not allowed in a ResolutionModel.'
        )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test that this isn't the case :) It does use the append_component from ResolutionModel, not from the base class.


from copy import copy
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a top-level import like elsewhere. No need to import it every time you call this method


if sample_model.Q is not None:
for index in range(len(sample_model.Q)):
resolution_model._component_collections[index] = copy(
sample_model.get_component_collection(Q_index=index)
)
if normalize_area:
resolution_model.normalize_area()

if fix_parameters:
resolution_model.fix_all_parameters()

return resolution_model
Loading
Loading