Skip to content

Commit

Permalink
Refactor/spectral model (#763)
Browse files Browse the repository at this point in the history
* Added SpectralShapeGaussian.
* Replaced property energy_spectrum of SpectralMegacomplex with invert and axis_scale.
* Made invert and axis_scale dataset properties.
* Added guard and meaningful exception message for spectral skewness
* Fixed scaling
* Made SpectralShapeSkewedGaussian decendent of SpectralShapeGaussian and
added fallback for skewness == 0.
* Move sanatize.py to utils module and correct typo sanatize -> sanitize (incl rename to sanitize.py)
* Move regex patterns to seperate module in utils
* Add sanity_scientific_notation_conversion
Convert scientific notation string (e.g. 1E7) to proper floats
* Fixed spectral megacomplex test parameters and added test for inverted axis
* Removed model_item._from_list
* Added convenience in model_item.from_dict for automatically convert float or int typed properties which are parsed as strings.
* Made amplitude of shape optional

Co-authored-by: Joris Snellenburg <[email protected]>
  • Loading branch information
joernweissenborn and jsnel committed Aug 14, 2021
1 parent 290e726 commit 5d24823
Show file tree
Hide file tree
Showing 17 changed files with 305 additions and 181 deletions.
21 changes: 16 additions & 5 deletions glotaran/builtin/io/yml/test/test_model_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from glotaran.builtin.megacomplexes.decay.decay_megacomplex import DecayMegacomplex
from glotaran.builtin.megacomplexes.decay.initial_concentration import InitialConcentration
from glotaran.builtin.megacomplexes.decay.irf import IrfMultiGaussian
from glotaran.builtin.megacomplexes.spectral.shape import SpectralShapeSkewedGaussian
from glotaran.builtin.megacomplexes.spectral.shape import SpectralShapeGaussian
from glotaran.io import load_model
from glotaran.model import DatasetModel
from glotaran.model import Model
Expand All @@ -25,7 +25,7 @@
def model():
spec_path = join(THIS_DIR, "test_model_spec.yml")
m = load_model(spec_path)
print(m.markdown())
print(m.markdown()) # noqa
return m


Expand All @@ -48,9 +48,20 @@ def test_dataset(model):
assert dataset.irf == "irf1"
assert dataset.scale == 1

assert "dataset2" in model.dataset
dataset = model.dataset["dataset2"]
assert isinstance(dataset, DatasetModel)
assert dataset.label == "dataset2"
assert dataset.megacomplex == ["cmplx2"]
assert dataset.initial_concentration == "inputD2"
assert dataset.irf == "irf2"
assert dataset.scale == 2
assert dataset.spectral_axis_scale == 1e7
assert dataset.spectral_axis_inverted


def test_constraints(model):
print(model.constraints)
print(model.constraints) # noqa
assert len(model.constraints) == 2

zero = model.constraints[0]
Expand All @@ -77,7 +88,7 @@ def test_penalties(model):


def test_relations(model):
print(model.relations)
print(model.relations) # noqa
assert len(model.relations) == 1

rel = model.relations[0]
Expand Down Expand Up @@ -154,7 +165,7 @@ def test_shapes(model):
assert "shape1" in model.shape

shape = model.shape["shape1"]
assert isinstance(shape, SpectralShapeSkewedGaussian)
assert isinstance(shape, SpectralShapeGaussian)
assert shape.amplitude.full_label == "shape.1"
assert shape.location.full_label == "shape.2"
assert shape.width.full_label == "shape.3"
Expand Down
4 changes: 3 additions & 1 deletion glotaran/builtin/io/yml/test/test_model_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dataset:
initial_concentration: inputD2
irf: irf2
scale: 2
spectral_axis_scale: 1e7
spectral_axis_inverted: true

irf:
irf1:
Expand Down Expand Up @@ -54,7 +56,7 @@ k_matrix:

shape:
shape1:
type: "skewed-gaussian"
type: "gaussian"
amplitude: shape.1
location: shape.2
width: shape.3
Expand Down
4 changes: 2 additions & 2 deletions glotaran/builtin/io/yml/yml.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

import yaml

from glotaran.builtin.io.yml.sanatize import check_deprecations
from glotaran.builtin.io.yml.sanatize import sanitize_yaml
from glotaran.io import ProjectIoInterface
from glotaran.io import load_dataset
from glotaran.io import load_model
Expand All @@ -21,6 +19,8 @@
from glotaran.parameter import ParameterGroup
from glotaran.project import SavingOptions
from glotaran.project import Scheme
from glotaran.utils.sanitize import check_deprecations
from glotaran.utils.sanitize import sanitize_yaml

if TYPE_CHECKING:
from glotaran.project import Result
Expand Down
75 changes: 27 additions & 48 deletions glotaran/builtin/megacomplexes/spectral/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,16 @@

@model_item(
properties={
"amplitude": Parameter,
"amplitude": {"type": Parameter, "allow_none": True},
"location": Parameter,
"width": Parameter,
"skewness": {"type": Parameter, "allow_none": True},
},
has_type=True,
)
class SpectralShapeSkewedGaussian:
"""A (skewed) Gaussian spectral shape"""
class SpectralShapeGaussian:
"""A Gaussian spectral shape"""

def calculate(self, axis: np.ndarray) -> np.ndarray:
r"""Calculate a (skewed) Gaussian shape for a given ``axis``.
If a non-zero ``skewness`` parameter was added
:func:`calculate_skewed_gaussian` will be used.
Otherwise it will use :func:`calculate_gaussian`.
Parameters
----------
axis: np.ndarray
The axis to calculate the shape for.
Returns
-------
shape: numpy.ndarray
A Gaussian shape.
See Also
--------
calculate_gaussian
calculate_skewed_gaussian
Note
----
Internally ``axis`` is converted from :math:`\mbox{nm}` to
:math:`1/\mbox{cm}`, thus ``location`` and ``width`` also need to
be provided in :math:`1/\mbox{cm}` (``1e7/value_in_nm``).
"""
return (
self.calculate_skewed_gaussian(axis)
if self.skewness is not None and not np.allclose(self.skewness, 0)
else self.calculate_gaussian(axis)
)

def calculate_gaussian(self, axis: np.ndarray) -> np.ndarray:
r"""Calculate a normal Gaussian shape for a given ``axis``.
The following equation is used for the calculation:
Expand Down Expand Up @@ -91,11 +55,22 @@ def calculate_gaussian(self, axis: np.ndarray) -> np.ndarray:
np.ndarray
An array representing a Gaussian shape.
"""
return self.amplitude * np.exp(
-np.log(2) * np.square(2 * (axis - self.location) / self.width)
)
shape = np.exp(-np.log(2) * np.square(2 * (axis - self.location) / self.width))
if self.amplitude is not None:
shape *= self.amplitude
return shape

def calculate_skewed_gaussian(self, axis: np.ndarray) -> np.ndarray:

@model_item(
properties={
"skewness": Parameter,
},
has_type=True,
)
class SpectralShapeSkewedGaussian(SpectralShapeGaussian):
"""A skewed Gaussian spectral shape"""

def calculate(self, axis: np.ndarray) -> np.ndarray:
r"""Calculate the skewed Gaussian shape for ``axis``.
The following equation is used for the calculation:
Expand Down Expand Up @@ -134,7 +109,7 @@ def calculate_skewed_gaussian(self, axis: np.ndarray) -> np.ndarray:
Note that in the limit of skewness parameter :math:`b` equal to zero
:math:`f(x, x_0, A, \Delta, b)` simplifies to a normal gaussian
(since :math:`\lim_{b \to 0} \frac{\ln(1+bx)}{b}=x`),
see the definition in :func:`calculate_gaussian`.
see the definition in :func:`SpectralShapeGaussian.calculate`.
Parameters
----------
Expand All @@ -147,14 +122,17 @@ def calculate_skewed_gaussian(self, axis: np.ndarray) -> np.ndarray:
np.ndarray
An array representing a skewed Gaussian shape.
"""
if np.allclose(self.skewness, 0):
return super().calculate(axis)
log_args = 1 + (2 * self.skewness * (axis - self.location) / self.width)
result = np.zeros(log_args.shape)
shape = np.zeros(log_args.shape)
valid_arg_mask = np.where(log_args > 0)
result[valid_arg_mask] = self.amplitude * np.exp(
shape[valid_arg_mask] = np.exp(
-np.log(2) * np.square(np.log(log_args[valid_arg_mask]) / self.skewness)
)

return result
if self.amplitude is not None:
shape *= self.amplitude
return shape


@model_item(properties={}, has_type=True)
Expand Down Expand Up @@ -201,6 +179,7 @@ def calculate(self, axis: np.ndarray) -> np.ndarray:

@model_item_typed(
types={
"gaussian": SpectralShapeGaussian,
"skewed-gaussian": SpectralShapeSkewedGaussian,
"one": SpectralShapeOne,
"zero": SpectralShapeZero,
Expand Down
11 changes: 8 additions & 3 deletions glotaran/builtin/megacomplexes/spectral/spectral_megacomplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@

@megacomplex(
dimension="spectral",
properties={"energy_spectrum": {"type": bool, "default": False}},
dataset_properties={
"spectral_axis_inverted": {"type": bool, "default": False},
"spectral_axis_scale": {"type": float, "default": 1},
},
model_items={
"shape": Dict[str, SpectralShape],
},
Expand All @@ -35,8 +38,10 @@ def calculate_matrix(
compartments.append(compartment)

model_axis = dataset_model.get_model_axis()
if self.energy_spectrum:
model_axis = 1e7 / model_axis
if dataset_model.spectral_axis_inverted:
model_axis = dataset_model.spectral_axis_scale / model_axis
elif dataset_model.spectral_axis_scale != 1:
model_axis = model_axis * dataset_model.spectral_axis_scale

dim1 = model_axis.size
dim2 = len(self.shape)
Expand Down
Loading

0 comments on commit 5d24823

Please sign in to comment.