Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
0ddd551
add coverage.yaml
henrikjacobsenfys Sep 18, 2025
76c8c5d
fix typo
henrikjacobsenfys Sep 18, 2025
91c294f
Add toml, gitignore and basic folder structure
henrikjacobsenfys Sep 18, 2025
c6e1deb
add dummy test
henrikjacobsenfys Sep 18, 2025
95ada73
add some dummy code to check if code coverage works
henrikjacobsenfys Sep 18, 2025
f13cf09
set up coverage
henrikjacobsenfys Sep 18, 2025
a35bcf5
Add BSD 3-Clause License
AndrewSazonov Sep 18, 2025
e8f64b9
Update pyproject.toml for hatchling build system
AndrewSazonov Sep 18, 2025
8ddf4da
move dummy code to add source folder
henrikjacobsenfys Sep 18, 2025
e7d0646
Merge branch 'add-actions' of https://github.com/easyscience/dynamics…
henrikjacobsenfys Sep 18, 2025
c685fc9
change dummy tests
henrikjacobsenfys Sep 18, 2025
746ec29
change import
henrikjacobsenfys Sep 18, 2025
4a67024
Enhance coverage report command in CI workflow
AndrewSazonov Sep 18, 2025
b0656ee
Create __init__.py
AndrewSazonov Sep 18, 2025
95fef34
Add GitHub Actions workflow for unit testing
AndrewSazonov Sep 18, 2025
e0b1264
remove dummies, add sample model and components
henrikjacobsenfys Sep 18, 2025
f3bcf6a
Fixing type issues
henrikjacobsenfys Sep 18, 2025
102d181
update typing, remove a method that will be introduced later
henrikjacobsenfys Sep 18, 2025
70e21f9
More type
henrikjacobsenfys Sep 18, 2025
d88ef70
Add example
henrikjacobsenfys Sep 18, 2025
d91b3ce
More type fixing
henrikjacobsenfys Sep 18, 2025
88c5f28
more typing...
henrikjacobsenfys Sep 19, 2025
2584ad8
Update detailed balancing
henrikjacobsenfys Sep 19, 2025
54b43cb
Update eample and detailed balance calculation
henrikjacobsenfys Sep 19, 2025
9db85c1
Update gitignore
henrikjacobsenfys Sep 19, 2025
cf7fb8e
Update example
henrikjacobsenfys Sep 19, 2025
53040c5
Update example
henrikjacobsenfys Sep 19, 2025
d87fcef
Remove pycache
henrikjacobsenfys Sep 19, 2025
428c04c
Remove features to make pull request smaller
henrikjacobsenfys Sep 19, 2025
ebe10f8
Merge branch 'develop' into ModelComponent
henrikjacobsenfys Sep 19, 2025
13b57b6
try to fix coverage
henrikjacobsenfys Sep 23, 2025
49a27b3
Merge branch 'ModelComponent' of https://github.com/easyscience/dynam…
henrikjacobsenfys Sep 23, 2025
6a15670
Fix some minor issues
henrikjacobsenfys Sep 23, 2025
297231d
Formatting
henrikjacobsenfys Sep 24, 2025
119926c
remove dummy code
henrikjacobsenfys Sep 24, 2025
d771784
Added many tests and updated a few style things
henrikjacobsenfys Sep 24, 2025
af9b7d9
More tests
henrikjacobsenfys Sep 24, 2025
b5bd527
And a few more tests
henrikjacobsenfys Sep 24, 2025
c0b69b4
rename folder to sample_model
henrikjacobsenfys Sep 29, 2025
39adf19
split compoments into different files
henrikjacobsenfys Sep 29, 2025
f9b7fa1
update type hinting
henrikjacobsenfys Sep 29, 2025
938a366
Update units, remove unused tests
henrikjacobsenfys Sep 29, 2025
c5f284d
Split tests into components
henrikjacobsenfys Sep 29, 2025
312eb8f
Update handling of incorrect units
henrikjacobsenfys Sep 29, 2025
6125967
Update warnings to handle parameter input
henrikjacobsenfys Sep 29, 2025
362f621
Remove Parameters as input :(
henrikjacobsenfys Sep 30, 2025
f270a1a
first step to making attributes
henrikjacobsenfys Sep 30, 2025
ebd604d
remove get_parameter
henrikjacobsenfys Sep 30, 2025
8d14075
changed class descriptions
henrikjacobsenfys Sep 30, 2025
de0ca56
Update minimum width to be nonzero
henrikjacobsenfys Sep 30, 2025
fe2f72d
Added minimum for area
henrikjacobsenfys Sep 30, 2025
75f1995
Update evaluate expressions
henrikjacobsenfys Sep 30, 2025
e824d06
Update copy
henrikjacobsenfys Sep 30, 2025
733da4d
Remove unneeded checks in evaluate
henrikjacobsenfys Sep 30, 2025
c812f0f
Update docstring of evaluate
henrikjacobsenfys Sep 30, 2025
6b6c63c
Update evaluate of Gaussian
henrikjacobsenfys Sep 30, 2025
32a84cd
Check NaN and inf
henrikjacobsenfys Sep 30, 2025
c80742d
Make equations slightly easier to read
henrikjacobsenfys Sep 30, 2025
82778b2
Add setters and getters for gaussian
henrikjacobsenfys Sep 30, 2025
20ce0fe
Update tests
henrikjacobsenfys Sep 30, 2025
948fd84
Update doc strings
henrikjacobsenfys Sep 30, 2025
223ad6b
Add getters and setters
henrikjacobsenfys Sep 30, 2025
f89a218
Update tests of gaussian
henrikjacobsenfys Sep 30, 2025
9adc26a
Add WHEN THEN EXPECT to tests
henrikjacobsenfys Sep 30, 2025
5ea9346
Add tests of the getters and setters
henrikjacobsenfys Sep 30, 2025
4fd8b31
Add tests to DHO
henrikjacobsenfys Sep 30, 2025
e902064
Update tests
henrikjacobsenfys Sep 30, 2025
47cd820
And a bit more test
henrikjacobsenfys Sep 30, 2025
72b9c6f
ruff
henrikjacobsenfys Oct 3, 2025
57c5811
Update units to allow scipp units
henrikjacobsenfys Oct 3, 2025
a92eb05
add WHEN THEN EXPECT to DHO
henrikjacobsenfys Oct 5, 2025
ac12a53
add WHEN THEN EXPECT to DeltaFunction
henrikjacobsenfys Oct 5, 2025
8a0bc03
add WHEN THEN EXPECT to Gaussian
henrikjacobsenfys Oct 5, 2025
51743ba
Add WHEN THEN EXPECT to Lorentzian
henrikjacobsenfys Oct 5, 2025
87bbe23
add more WHEN THEN EXPECT to tests
henrikjacobsenfys Oct 5, 2025
6b9307a
fixed warnings in tests
henrikjacobsenfys Oct 5, 2025
a1f7356
Update polynomial to accept units
henrikjacobsenfys Oct 5, 2025
58db3cb
Add some explanation to polynomial component
henrikjacobsenfys Oct 6, 2025
7a8ed0c
rename a few tests
henrikjacobsenfys Oct 7, 2025
b2705b7
Merge branch 'develop' into ModelComponent
henrikjacobsenfys Oct 7, 2025
8c17672
make evaluate a bit easier
henrikjacobsenfys Oct 8, 2025
3a0a97e
Handle sc.DataArray as input
henrikjacobsenfys Oct 8, 2025
68e1a3c
Add some tests and handle some comments
henrikjacobsenfys Oct 8, 2025
8f24251
Merge branch 'ModelComponent' of https://github.com/easyscience/dynam…
henrikjacobsenfys Oct 8, 2025
4b58ff1
Update delta function
henrikjacobsenfys Oct 10, 2025
b6a2305
Add checks to prevent setting negative widths
henrikjacobsenfys Oct 10, 2025
c7c57e5
Move unit to ModelComponent
henrikjacobsenfys Oct 14, 2025
96c4449
Update polynomial, various small fixes
henrikjacobsenfys Oct 14, 2025
7995642
Update tests
henrikjacobsenfys Oct 14, 2025
c494908
parametrize
henrikjacobsenfys Oct 14, 2025
51d2b48
Parametrize tests of setters
henrikjacobsenfys Oct 14, 2025
f0b0eac
Delete unneeded tests
henrikjacobsenfys Oct 14, 2025
db40172
Update test of get_parameters
henrikjacobsenfys Oct 14, 2025
677e3e5
Update polynomial
henrikjacobsenfys Oct 14, 2025
04488a4
Add EPSILON to delta function
henrikjacobsenfys Oct 14, 2025
a724989
Update delta function and make x sorted
henrikjacobsenfys Oct 14, 2025
51563de
Update polynomial
henrikjacobsenfys Oct 14, 2025
30fea3c
Update copy
henrikjacobsenfys Oct 14, 2025
73ab72a
Final fixes
henrikjacobsenfys Oct 14, 2025
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ examples/INS_example/*
examples/Anesthetics
src/easydynamics/__pycache__
.vscode/*
**/__pycache__/*
**/__pycache__/*
136 changes: 136 additions & 0 deletions examples/component_example.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "64deaa41",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"from easydynamics.sample_model import Gaussian\n",
"from easydynamics.sample_model import Lorentzian\n",
"from easydynamics.sample_model import DampedHarmonicOscillator\n",
"from easydynamics.sample_model import Polynomial\n",
"from easydynamics.sample_model import DeltaFunction\n",
"\n",
"\n",
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
"%matplotlib widget"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "784d9e82",
"metadata": {},
"outputs": [],
"source": [
"# Creating a component\n",
"gaussian=Gaussian(name='Gaussian',width=0.5,area=1)\n",
"dho = DampedHarmonicOscillator(name='DHO',center=1.0,width=0.3,area=2.0)\n",
"lorentzian = Lorentzian(name='Lorentzian',center=-1.0,width=0.2,area=1.0)\n",
"polynomial = Polynomial(name='Polynomial',coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2\n",
"\n",
"x=np.linspace(-2, 2, 100)\n",
"\n",
"plt.figure()\n",
"y=gaussian.evaluate(x)\n",
"plt.plot(x, y, label='Gaussian')\n",
"y=dho.evaluate(x)\n",
"plt.plot(x, y, label='DHO')\n",
"y=lorentzian.evaluate(x)\n",
"plt.plot(x, y, label='Lorentzian')\n",
"y=polynomial.evaluate(x)\n",
"plt.plot(x, y, label='Polynomial')\n",
"plt.legend()\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2f57228c",
"metadata": {},
"outputs": [],
"source": [
"# The area under the DHO curve is indeed equal to the area parameter.\n",
"xx=np.linspace(-15, 15, 10000)\n",
"yy=dho.evaluate(xx)\n",
"area= np.trapezoid(yy, xx)\n",
"print(f\"Area under DHO curve: {area:.4f}\")\n",
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6c0929ed",
"metadata": {},
"outputs": [],
"source": [
"delta = DeltaFunction(name='Delta', center=0.0, area=1.0)\n",
"x1=np.linspace(-2, 2, 100)\n",
"y=delta.evaluate(x1)\n",
"x2=np.linspace(-2,2,51)\n",
"y2=delta.evaluate(x2)\n",
"plt.figure()\n",
"plt.plot(x1, y, label='Delta Function')\n",
"plt.plot(x2, y2, label='Delta Function (coarser)')\n",
"plt.legend()\n",
"plt.show()\n",
"# The area under the Delta function is indeed equal to the area parameter.\n",
"xx=np.linspace(-2, 2, 10000)\n",
"yy=delta.evaluate(xx)\n",
"area= np.trapezoid(y, x1)\n",
"print(area)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f44b125a",
"metadata": {},
"outputs": [],
"source": [
"import scipp as sc\n",
"x1=sc.linspace(dim='x', start=-2.0, stop=2.0, num=100, unit='meV')\n",
"x2=sc.linspace(dim='x', start=-2.0*1e3, stop=2.0*1e3, num=101, unit='microeV')\n",
"\n",
"polynomial = Polynomial(name='Polynomial',coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2\n",
"y1=polynomial.evaluate(x1)\n",
"y2=polynomial.evaluate(x2)\n",
"\n",
"plt.figure()\n",
"plt.plot(x1.values, y1, label='Polynomial meV',color='blue')\n",
"plt.plot(x2.values/1000, y2, label='Polynomial microeV',linestyle='dashed',color='orange')\n",
"plt.legend()\n",
"plt.show()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "newdynamics",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.13"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
18 changes: 18 additions & 0 deletions src/easydynamics/sample_model/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .components import (
DampedHarmonicOscillator,
DeltaFunction,
Gaussian,
Lorentzian,
Polynomial,
Voigt,
)

__all__ = [
"SampleModel",
"Gaussian",
"Lorentzian",
"Voigt",
"DeltaFunction",
"DampedHarmonicOscillator",
"Polynomial",
]
15 changes: 15 additions & 0 deletions src/easydynamics/sample_model/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .damped_harmonic_oscillator import DampedHarmonicOscillator
from .delta_function import DeltaFunction
from .gaussian import Gaussian
from .lorentzian import Lorentzian
from .polynomial import Polynomial
from .voigt import Voigt

__all__ = [
"Gaussian",
"Lorentzian",
"Voigt",
"DeltaFunction",
"DampedHarmonicOscillator",
"Polynomial",
]
181 changes: 181 additions & 0 deletions src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from __future__ import annotations

import warnings
from typing import Union

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

from .model_component import ModelComponent

Numeric = Union[float, int]

MINIMUM_WIDTH = 1e-10 # To avoid division by zero


class DampedHarmonicOscillator(ModelComponent):
"""
Damped Harmonic Oscillator (DHO). 2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 )

Args:
name (str): Name of the component.
center (Int or float): Resonance frequency, approximately the peak position.
width (Int or float): Damping constant, approximately the half width at half max (HWHM) of the peaks.
area (Int or float): Area under the curve.
unit (str or sc.Unit): Unit of the parameters. Defaults to "meV".
"""

def __init__(
self,
name: str = "DHO",
center: Numeric = 1.0,
width: Numeric = 1.0,
area: Numeric = 1.0,
unit: Union[str, sc.Unit] = "meV",
):
# Validate inputs
if not isinstance(area, Numeric):
raise TypeError("area must be a number.")
area = float(area)
if area < 0:
warnings.warn(
"The area of the Damped Harmonic Oscillator with name {} is negative, which may not be physically meaningful.".format(
name
)
)

if not isinstance(center, Numeric):
raise TypeError("center must be a number.")

center = float(center)

if not isinstance(width, Numeric):
raise TypeError("width must be a number.")

width = float(width)
if width <= 0:
raise ValueError(
"The width of a DampedHarmonicOscillator must be greater than zero."
)

super().__init__(name=name, unit=unit)

# Create Parameters from floats
self._area = Parameter(name=name + " area", value=area, unit=unit)
if area > 0:
self._area.min = 0.0

self._center = Parameter(name=name + " center", value=center, unit=unit)

self._width = Parameter(
name=name + " width", value=width, unit=unit, min=MINIMUM_WIDTH
)

@property
def area(self) -> Parameter:
"""Return the area parameter."""
return self._area

@area.setter
def area(self, value: Numeric):
"""Set the area parameter."""
if not isinstance(value, Numeric):

Choose a reason for hiding this comment

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

You might want a check here to check if the value being set is lower than the minimum of 0.0. I will be caught by the Parameter, but you can raise a more informative warning message if you check it here.

raise TypeError("area must be a number.")
value = float(value)
if value < 0:
warnings.warn(
"The area of the Damped Harmonic Oscillator with name {} is negative, which may not be physically meaningful.".format(
self.name
)
)
self._area.value = float(value)

@property
def center(self) -> Parameter:
"""Return the center parameter."""
return self._center

@center.setter
def center(self, value: Numeric):
"""Set the center parameter."""
if not isinstance(value, Numeric):
raise TypeError("center must be a number.")
self._center.value = float(value)

@property
def width(self) -> Parameter:

Choose a reason for hiding this comment

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

Ales raised a valid point earlier, since this getter returns the Parameter itself, the user will be able to set a negative width by using the returned Parameter. I propose to overwrite the value setter method of the created width Parameter in order to have this check directly in it. Then you can also ignore my next comment ;)

"""Return the width parameter."""
return self._width

@width.setter
def width(self, value: Numeric):
"""Set the width parameter."""
if not isinstance(value, Numeric):
raise TypeError("width must be a number.")

Choose a reason for hiding this comment

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

You need to check that the user does not set a negative width here.

value = float(value)
if value <= 0:
raise ValueError(
"The width of a DampedHarmonicOscillator must be greater than zero."
)
self._width.value = value

Choose a reason for hiding this comment

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

You also forgot a property for unit here.

def evaluate(
self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
) -> np.ndarray:
"""Evaluate the Damped Harmonic Oscillator at the given x values.
If x is a scipp Variable, the unit of the DHO will be converted to
match x. The DHO evaluates to 2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 )"""

x = self._prepare_x_for_evaluate(x)

normalization = 2 * self._center.value**2 * self._width.value / np.pi
denominator = (x**2 - self._center.value**2) ** 2 + (
2
* self._width.value
* x # No division by zero here, width>0 enforced in setter
) ** 2

return self._area.value * normalization / (denominator)

Choose a reason for hiding this comment

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

Since you're dividing you need to check for zero's and raise a proper warning. ex. if x_in = center and width=0, then denominator = 0.

Copy link
Member Author

Choose a reason for hiding this comment

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

But the minimum value of the width is MINIMUM_WIDTH=1e-10


def get_parameters(self):
"""
Get all parameters from the model component.
Returns:
List[Parameter]: List of parameters in the component.
"""
return [self._area, self._center, self._width]

def convert_unit(self, unit: Union[str, sc.Unit]):
"""
Convert the unit of the Parameters in the component.

Args:
unit (str or sc.Unit): The new unit to convert to.
"""

self._area.convert_unit(unit)
self._center.convert_unit(unit)
self._width.convert_unit(unit)
self._unit = unit
Copy link

Choose a reason for hiding this comment

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

in init units accepted as both str and sc.Unit, but here it is always converted to str. Does it have to be both units? Or is simply str preferrable?

Copy link
Member Author

Choose a reason for hiding this comment

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

str or sc.Unit should be allowed, will update


def __copy__(self) -> DampedHarmonicOscillator:
"""
Return a deep copy of this component with independent parameters.
"""
name = "copy of " + self.name

model_copy = DampedHarmonicOscillator(
name=name,
area=self._area.value,
center=self._center.value,
width=self._width.value,
unit=self._unit,
)
model_copy._area.fixed = self._area.fixed
model_copy._center.fixed = self._center.fixed
model_copy._width.fixed = self._width.fixed
return model_copy

Choose a reason for hiding this comment

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

Hmm, you're not copying if the user had any min/max set on their parameters, or dependency expressions. A better way of copying might be to rely on serialization, by serializing the object, removing the unique_names from the resulting dict and then de-serializing to a new object. But we currently can't serialize and de-serialize dependent Parameters, to maybe just add an issue on this for now?
You could still use the serialization/deserialization approach here, it should still be more scalable and transferable than your approach.


def __repr__(self):
return f"DampedHarmonicOscillator(name = {self.name}, unit = {self._unit},\n area = {self._area},\n center = {self._center},\n width = {self._width})"
Loading