Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

128 Resolution smearing Refnx #134

Merged
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
4 changes: 4 additions & 0 deletions EasyReflectometry/calculators/calculator_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from abc import ABCMeta
from typing import Callable

import numpy as np
from easyCore.Objects.core import ComponentSerializer
Expand Down Expand Up @@ -173,3 +174,6 @@ def sld_profile(self, model_id: str) -> tuple[np.ndarray, np.ndarray]:
:return: z and sld(z)
"""
return self._wrapper.sld_profile(model_id)

def set_resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None:
return self._wrapper.set_resolution_function(resolution_function)
15 changes: 10 additions & 5 deletions EasyReflectometry/calculators/refl1d/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from refl1d import model
from refl1d import names

from EasyReflectometry.experiment.resolution_functions import is_constant_resolution_function

from ..wrapper_base import WrapperBase

PADDING_RANGE = 3.5
Expand Down Expand Up @@ -162,17 +164,20 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray:
argmin = np.argmin(q_array)
argmax = np.argmax(q_array)
dq_vector = self._resolution_function(q_array)
dq_vector_normalized_to_refnx = dq_vector * q_array / 100 / (2 * np.sqrt(2 * np.log(2)))

if is_constant_resolution_function(self._resolution_function):
# Get percentage of Q and change from sigma to FWHM
dq_vector = dq_vector * q_array / 100 / (2 * np.sqrt(2 * np.log(2)))

q = names.QProbe(
q_array,
dq_vector_normalized_to_refnx,
Q=q_array,
dQ=dq_vector,
intensity=self.storage['model'][model_name]['scale'],
background=self.storage['model'][model_name]['bkg'],
)
q.calc_Qo = np.linspace(
q_array[argmin] - PADDING_RANGE * dq_vector_normalized_to_refnx[argmin],
q_array[argmax] + PADDING_RANGE * dq_vector_normalized_to_refnx[argmax],
q_array[argmin] - PADDING_RANGE * dq_vector[argmin],
q_array[argmax] + PADDING_RANGE * dq_vector[argmax],
UPSCALE_FACTOR * len(q_array),
)
R = names.Experiment(probe=q, sample=structure).reflectivity()[1]
Expand Down
13 changes: 11 additions & 2 deletions EasyReflectometry/calculators/refnx/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from easyCore import np
from refnx import reflect

from EasyReflectometry.experiment.resolution_functions import is_constant_resolution_function

from ..wrapper_base import WrapperBase


Expand Down Expand Up @@ -125,9 +127,16 @@ def calculate(self, q_array: np.ndarray, model_name: str) -> np.ndarray:
structure,
scale=self.storage['model'][model_name].scale.value,
bkg=self.storage['model'][model_name].bkg.value,
dq=self.storage['model'][model_name].dq.value,
dq_type='pointwise',
)
return model(q_array)

dq_vector = self._resolution_function(q_array)
if is_constant_resolution_function(self._resolution_function):
# FWHM Percentage resolution is constant given as
# For a constant resolution percentage refnx supports to pass a scalar value rather than a vector
dq_vector = dq_vector[0]

return model(x=q_array, x_err=dq_vector)

def sld_profile(self, model_name: str) -> Tuple[np.ndarray, np.ndarray]:
"""
Expand Down
6 changes: 4 additions & 2 deletions EasyReflectometry/calculators/wrapper_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

from EasyReflectometry.experiment import constant_resolution_function

DEFAULT_RESOLUTION_FWHM_PERCENTAGE = 5.0


class WrapperBase:
def __init__(self):
Expand All @@ -15,7 +17,7 @@ def __init__(self):
'item': {},
'model': {},
}
self._resolution_function = constant_resolution_function(5)
self._resolution_function = constant_resolution_function(DEFAULT_RESOLUTION_FWHM_PERCENTAGE)

def reset_storage(self):
"""Reset the storage area to blank."""
Expand Down Expand Up @@ -204,7 +206,7 @@ def get_item_value(self, name: str, key: str) -> float:
item = getattr(item, key)
return getattr(item, 'value')

def set_resolution_function(self, resolution_function: Callable[[np.array], float]) -> None:
def set_resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None:
"""Set the resolution function for the calculator.

:param resolution_function: The resolution function
Expand Down
37 changes: 31 additions & 6 deletions EasyReflectometry/experiment/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from easyCore.Objects.ObjectClasses import BaseObj
from easyCore.Objects.ObjectClasses import Parameter

from EasyReflectometry.experiment.resolution_functions import is_constant_resolution_function
from EasyReflectometry.parameter_utils import get_as_parameter
from EasyReflectometry.sample import BaseAssembly
from EasyReflectometry.sample import Layer
Expand Down Expand Up @@ -86,8 +87,11 @@ def __init__(
scale=scale,
background=background,
)
if not callable(resolution_function):
raise ValueError('Resolution function must be a callable.')
self.resolution_function = resolution_function
# Must be set after resolution function
self.interface = interface
self._resolution_function = resolution_function

def add_item(self, *assemblies: list[BaseAssembly]) -> None:
"""Add a layer or item to the model sample.
Expand Down Expand Up @@ -133,12 +137,33 @@ def remove_item(self, idx: int) -> None:
self.interface().remove_item_from_model(self.sample[idx].uid, self.uid)
del self.sample[idx]

def set_resolution_function(self, resolution_function: Callable[[np.array], float]) -> None:
"""Set the resolution function for the model.
@property
def resolution_function(self) -> Callable[[np.array], np.array]:
"""Return the resolution function."""
return self._resolution_function

:param resolution_function: Resolution function to set.
"""
@resolution_function.setter
def resolution_function(self, resolution_function: Callable[[np.array], np.array]) -> None:
"""Set the resolution function for the model."""
self._resolution_function = resolution_function
if self.interface is not None:
self.interface().set_resolution_function(self._resolution_function)

@property
def interface(self):
"""
Get the current interface of the object
"""
return self._interface

@interface.setter
def interface(self, new_interface) -> None:
"""Set the interface for the model."""
# From super class
self._interface = new_interface
if new_interface is not None:
self.generate_bindings()
self._interface().set_resolution_function(self._resolution_function)

@property
def uid(self) -> int:
Expand All @@ -149,7 +174,7 @@ def uid(self) -> int:
@property
def _dict_repr(self) -> dict[str, dict[str, str]]:
"""A simplified dict representation."""
if self._resolution_function.__qualname__.split('.')[0] == 'constant_resolution_function':
if is_constant_resolution_function(self._resolution_function):
resolution_value = self._resolution_function([0])[0]
resolution = f'{resolution_value} %'
else:
Expand Down
17 changes: 15 additions & 2 deletions EasyReflectometry/experiment/resolution_functions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
"""Resolution functions for the resolution of the experiment.
When a percentage is provided we assume that the resolution is a
Gaussian distribution with a FWHM of the percentage of the q value.
To convert from a sigma value to a FWHM value we use the formula
FWHM = 2.35 * sigma [2 * np.sqrt(2 * np.log(2)) * sigma].
"""

from typing import Callable
from typing import Union

import numpy as np

Expand All @@ -9,12 +17,12 @@ def constant_resolution_function(constant: float) -> Callable[[np.array], np.arr
:param constant: The constant resolution value.
"""

def _constant(q: np.array) -> np.array:
def _constant(q: Union[np.array, float]) -> np.array:
"""Function that calculates the resolution at a given q value.

The function uses the data points from the encapsulating function and produces a linearly interpolated between them.
"""
return np.ones(len(q)) * constant
return np.ones(np.array(q).size) * constant

return _constant

Expand All @@ -34,3 +42,8 @@ def _linear(q: np.array) -> np.array:
return np.interp(q, q_data_points, resolution_points)

return _linear


def is_constant_resolution_function(resolution_function: Callable[[np.array], np.array]) -> bool:
"""Check if the resolution function is a constant."""
return 'constant' in resolution_function.__name__
2 changes: 1 addition & 1 deletion docs/tutorials/_static/repeating_layers.ort
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# start_date: 2022-02-01
# title: Ni-Ti Multilayer
# instrument: Simulation
# probe: x-rays
# probe: x-ray
# sample:
# name: Ni-Ti Multilayer on Si
# category: gas / lsolidiquid
Expand Down
4 changes: 1 addition & 3 deletions tests/calculators/refl1d/test_refl1d_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ def test_calculate(self):
p.update_material('Material3', rho=4.000, irho=0.000)
p.create_model('MyModel')
p.update_model('MyModel', bkg=1e-7)
p.update_model('MyModel', bkg=1e-7, dq=5.0)
p.create_layer('Layer1')
p.assign_material_to_layer('Material1', 'Layer1')
p.create_layer('Layer2')
Expand Down Expand Up @@ -209,7 +208,7 @@ def test_calculate(self):
]
assert_almost_equal(p.calculate(q, 'MyModel'), expected)

def test_calculate2(self):
def test_calculate_three_items(self):
p = Refl1dWrapper()
p.create_material('Material1')
p.update_material('Material1', rho=0.000, irho=0.000)
Expand Down Expand Up @@ -252,7 +251,6 @@ def test_calculate2(self):
2.5347996e-07,
]
assert_almost_equal(p.calculate(q, 'MyModel'), expected)
assert_almost_equal(p.calculate(q, 'MyModel'), expected)

def test_sld_profile(self):
p = Refl1dWrapper()
Expand Down
Loading
Loading