-
Notifications
You must be signed in to change notification settings - Fork 1
Model component #50
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
Model component #50
Changes from all commits
0ddd551
76c8c5d
91c294f
c6e1deb
95ada73
f13cf09
a35bcf5
e8f64b9
8ddf4da
e7d0646
c685fc9
746ec29
4a67024
b0656ee
95fef34
e0b1264
f3bcf6a
102d181
70e21f9
d88ef70
d91b3ce
88c5f28
2584ad8
54b43cb
9db85c1
cf7fb8e
53040c5
d87fcef
428c04c
ebe10f8
13b57b6
49a27b3
6a15670
297231d
119926c
d771784
af9b7d9
b5bd527
c0b69b4
39adf19
f9b7fa1
938a366
c5f284d
312eb8f
6125967
362f621
f270a1a
ebd604d
8d14075
de0ca56
fe2f72d
75f1995
e824d06
733da4d
c812f0f
6b6c63c
32a84cd
c80742d
82778b2
20ce0fe
948fd84
223ad6b
f89a218
9adc26a
5ea9346
4fd8b31
e902064
47cd820
72b9c6f
57c5811
a92eb05
ac12a53
8a0bc03
51743ba
87bbe23
6b9307a
a1f7356
58db3cb
7a8ed0c
b2705b7
8c17672
3a0a97e
68e1a3c
8f24251
4b58ff1
b6a2305
c7c57e5
96c4449
7995642
c494908
51d2b48
f0b0eac
db40172
677e3e5
04488a4
a724989
51563de
30fea3c
73ab72a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } |
| 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", | ||
| ] |
| 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", | ||
| ] |
| 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.") | ||
seventil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
seventil marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @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: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
seventil marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
|
|
||
| def __repr__(self): | ||
| return f"DampedHarmonicOscillator(name = {self.name}, unit = {self._unit},\n area = {self._area},\n center = {self._center},\n width = {self._width})" | ||
Uh oh!
There was an error while loading. Please reload this page.