diff --git a/examples/component_collection.ipynb b/examples/component_collection.ipynb
new file mode 100644
index 0000000..d50197d
--- /dev/null
+++ b/examples/component_collection.ipynb
@@ -0,0 +1,131 @@
+{
+ "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",
+ "\n",
+ "from easydynamics.sample_model import ComponentCollection\n",
+ "\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "\n",
+ "%matplotlib widget"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f2d27900",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from scipy.integrate import simpson\n",
+ "\n",
+ "model = ComponentCollection(display_name=\"TestComponentCollection\")\n",
+ "component1 = Gaussian(\n",
+ " display_name=\"TestGaussian1\",\n",
+ " area=1.0,\n",
+ " center=0.0,\n",
+ " width=1.0,\n",
+ " unit=\"meV\",\n",
+ " unique_name=\"TestGaussian1\",\n",
+ ")\n",
+ "component2 = Lorentzian(\n",
+ " display_name=\"TestLorentzian1\",\n",
+ " area=2.0,\n",
+ " center=1.0,\n",
+ " width=0.5,\n",
+ " unit=\"meV\",\n",
+ " unique_name=\"TestLorentzian1\",\n",
+ ")\n",
+ "model.add_component(component1)\n",
+ "model.add_component(component2)\n",
+ "\n",
+ "model.normalize_area()\n",
+ "# EXPECT\n",
+ "x = np.linspace(-10000, 10000, 1000000) # Lorentzians have long tails\n",
+ "result = model.evaluate(x)\n",
+ "numerical_area = simpson(result, x)\n",
+ "\n",
+ "print(numerical_area)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fe3b8780",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model.components[1].area"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "784d9e82",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "component_collection=ComponentCollection()\n",
+ "\n",
+ "# Creating components\n",
+ "gaussian=Gaussian(display_name='Gaussian',width=0.5,area=1)\n",
+ "dho = DampedHarmonicOscillator(display_name='DHO',center=1.0,width=0.3,area=2.0)\n",
+ "lorentzian = Lorentzian(display_name='Lorentzian',center=-1.0,width=0.2,area=1.0)\n",
+ "polynomial = Polynomial(display_name='Polynomial',coefficients=[0.1, 0, 0.5]) # y=0.1+0.5*x^2\n",
+ "\n",
+ "# Adding components to the component collection\n",
+ "component_collection.add_component(gaussian)\n",
+ "component_collection.add_component(dho)\n",
+ "component_collection.add_component(lorentzian)\n",
+ "component_collection.add_component(polynomial)\n",
+ "\n",
+ "x=np.linspace(-2, 2, 100)\n",
+ "\n",
+ "plt.figure()\n",
+ "y=component_collection.evaluate(x)\n",
+ "plt.plot(x, y, label='Component collection')\n",
+ "\n",
+ "for component in component_collection.components:\n",
+ " y = component.evaluate(x)\n",
+ " plt.plot(x, y, label=component.display_name)\n",
+ "\n",
+ "plt.legend()\n",
+ "plt.show()"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "easydynamics_newbase",
+ "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.12.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/component_example.ipynb b/examples/components.ipynb
similarity index 83%
rename from examples/component_example.ipynb
rename to examples/components.ipynb
index bdda8cf..26bb47c 100644
--- a/examples/component_example.ipynb
+++ b/examples/components.ipynb
@@ -19,7 +19,7 @@
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
- "%matplotlib widget"
+ "%matplotlib widget\n"
]
},
{
@@ -30,10 +30,10 @@
"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",
+ "gaussian=Gaussian(display_name='Gaussian',width=0.5,area=1)\n",
+ "dho = DampedHarmonicOscillator(display_name='DHO',center=1.0,width=0.3,area=2.0)\n",
+ "lorentzian = Lorentzian(display_name='Lorentzian',center=-1.0,width=0.2,area=1.0)\n",
+ "polynomial = Polynomial(display_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",
@@ -72,7 +72,7 @@
"metadata": {},
"outputs": [],
"source": [
- "delta = DeltaFunction(name='Delta', center=0.0, area=1.0)\n",
+ "delta = DeltaFunction(display_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",
@@ -100,7 +100,7 @@
"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",
+ "polynomial = Polynomial(display_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",
@@ -114,7 +114,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "newdynamics",
+ "display_name": "easydynamics_newbase",
"language": "python",
"name": "python3"
},
@@ -128,7 +128,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.13"
+ "version": "3.12.12"
}
},
"nbformat": 4,
diff --git a/examples/detailed_balance.ipynb b/examples/detailed_balance.ipynb
index 172422f..b4ca072 100644
--- a/examples/detailed_balance.ipynb
+++ b/examples/detailed_balance.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": null,
"id": "97050b3e",
"metadata": {},
"outputs": [],
@@ -17,36 +17,10 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"id": "c1654720",
"metadata": {},
- "outputs": [
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "7cfd67c54e984f0bbf333f80d81e1929",
- "version_major": 2,
- "version_minor": 0
- },
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfQBJREFUeJzt3Qd8E+X/B/Bv96KlzLLKLLKnCKIMUWQpwwGoKIgD9QcOBBkqw4GooPJXURQVF6iIDCeIyFLZe++9V/du7v/6PO2FJE1L2qa9jM8bz14ul8tzI3ffe9b5aJqmCRERERF5DV+jE0BEREREJYsBIBEREZGXYQBIRERE5GUYABIRERF5GQaARERERF6GASARERGRl2EASERERORlGAASEREReRkGgERERERehgEgERERkZdhAEhERETkZRgAEhEREXkZBoBEREREXoYBIBEREZGXYQBIRERE5GUYABIRERF5GQaARERERF6GASARERGRl2EASERERORlGAASEREReRkGgERERERehgEgERERkZdhAEhERETkZRgAEhEREXkZBoBEREREXoYBIBEREZGXYQBIRERE5GUYABIRERF5GQaARERERF6GASARERGRl2EASERERORlGAASEREReRkGgERERERehgEgERERkZdxSgC4YsUK8fHxUX/zM3HiRDXfxYsXxVluueUWNbi7zMxMGTVqlERHR4uvr6/06dNHPEFiYqI89thjUqlSJbXvn3vuOUPSge/G8af78ssv1bSjR49azTdlyhSpXbu2+Pn5SfPmzT163xB5AnvXn4cfflhq1qxZounAuQTpwLmFyB0wB9BFfPHFFyr4uPfee+Wrr76S4cOHO/07PvrooxI/Ob3xxhvqO5966in55ptv5KGHHhJX9eeff6pA7+abb5ZZs2aptJfUvnGW33//3SrQdcVjwh0kJyer7Xitm1qigtq9e7c6tmxvPp1xsz1hwgTp1q2blC1bttiC0f/++0+lPzY21unLppLlX8LfR3n4+++/pWrVqvLee+8V23fgYl++fHl1d1yS63XjjTeqE5MrQSB63333SVBQkFVakcP3+eefS2BgYInuG2cGgNOnT3c4CDTimHCXAPCVV15R455QwuBtZs6cKSaTSVw1AMSxhePKmbmUKFl79dVXpXr16tKsWbNiu3lBAIj045wRGRlZLN9B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TFB/PRHXz/15+jpy/dyfp68j14+Kgo1AiIiIiLwMcwCJiIiIvAwDQCIiIiIvwwCQiIiIyMswACQiIiLyMgwADTJp0iS56aabJDQ0VCIjIx36DNrrjB8/XipXriwhISHSuXNnOXDggNU8ly9flgEDBqhOM7HcRx99VBITE6WkFTQdR48eVU9TsTf8+OOP5vnsvf/999+LEQqzrW+55ZZc6X/yySet5jl+/Ljccccd6tioWLGivPDCC5KZmSmuvn6Y/+mnn5Z69eqp47N69eryzDPPSFxcnNV8Ru7D6dOnS82aNSU4OFjatGkj69evz3d+HHv169dX8zdp0kR+//33Av8mS1JB1m/mzJnSvn17KVOmjBqQdtv5H3744Vz7qlu3buIO6/fll1/mSjs+58r7r6DraO98ggHnD1fch6tWrZKePXuqp28gHQsXLrzmZ1asWCEtW7ZULYFjYmLUfi3q75pyoBUwlbzx48dr7777rvb8889rpUuXdugzb775ppp34cKF2rZt27RevXpptWrV0lJSUszzdOvWTWvWrJm2du1abfXq1VpMTIx2//33ayWtoOnIzMzUzpw5YzW88sorWqlSpbSEhATzfDhkZ82aZTWf5fqXpMJs644dO2qPP/64Vfrj4uKstkPjxo21zp07a1u2bNF+//13rXz58trYsWM1V1+/HTt2aHfffbf2888/awcPHtSWLVum1a1bV7vnnnus5jNqH37//fdaYGCg9sUXX2i7du1S+yEyMlI7d+6c3fn//fdfzc/PT3v77be13bt3ay+//LIWEBCg1rMgv8mSUtD1e+CBB7Tp06er42zPnj3aww8/rNbl5MmT5nkGDRqkjgPLfXX58mXNCAVdPxxjERERVmk/e/as1TyutP8Ks46XLl2yWr+dO3eqYxbr7or7EOezl156SZs/f746DyxYsCDf+Q8fPqyFhoaq6yR+gx988IFav8WLFxd6m9FVDAANhh+qIwGgyWTSKlWqpE2ZMsU8LTY2VgsKCtK+++479Ro/EPyoNmzYYJ7njz/+0Hx8fLRTp05pJcVZ6WjevLn2yCOPWE1z5KThyuuIAPDZZ5/N9wTp6+trdaH6+OOP1YUsLS1Nc7d9OHfuXHVyzsjIMHwftm7dWhs6dKj5dVZWllalShVt8uTJdufv16+fdscdd1hNa9OmjfbEE084/Jt05fWz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",
- "text/html": [
- "\n",
- "
\n",
- "
\n",
- " Figure\n",
- "
\n",
- "

\n",
- "
\n",
- " "
- ],
- "text/plain": [
- "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"\n",
"temperatures=[1, 10, 100]\n",
@@ -68,36 +42,10 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": null,
"id": "a64fbe7c",
"metadata": {},
- "outputs": [
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "16184f6dae4a40ea85c0c8ca1c716fd3",
- "version_major": 2,
- "version_minor": 0
- },
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZQVJREFUeJzt3Qd8FGX+x/EnPSH0DoIU8ayo2PXsDUQBy9k9sZyeimJX8BRsWLGdp2IvJ6JYsPw9Uey9IlhBRBSUXgPpZf6v75PMMrvZJLvJJlvm8369Jrs7O5l9pv/maZPmOI5jAAAA4Bvp8U4AAAAAWhYBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEADGwamnnmr69u0bNC4tLc1cc801MfuN9957z85Tr/XRb2q6lStXxuy399tvPzsku4qKCnP55Zeb3r17m/T0dHPEEUeYVLBhwwbzj3/8w3Tv3t1u+wsvvDAu6Qjd5x9//HE77rfffgua7rbbbjP9+/c3GRkZZocddkjpbQMgNnSN1bU22mtirCXy9TAlA0D3QuIOubm5pmfPnmbw4MHm3//+t1m/fn2j5/3jjz/ai1boRQqp59FHH7XBx9/+9jfzxBNPmIsuuijmv3HffffZ/bUl3XjjjfY3zznnHPPf//7X/P3vfzeJ6s0337SB3l//+lfz2GOP2bS31LaJlf/9739R3dzFY59IBkVFRXY9tvQFPNlwjUKkMk0Ku+6660y/fv1MeXm5Wbp0qT1xKLfjjjvuMK+88orZbrvtGnVwXXvttTaiD83Fi9RDDz1kqqqqGvW/aDnvvPOO2WSTTcydd97ZbL+hi33nzp2D7lRbYrl23313M378eJNIFIgef/zxJicnJyityuF75JFHTHZ2dotum1gGgPfee2/EQWA89olkCQB17pVEzVFJBLG4RqWiffbZxxQXFwedR/wupQPAQw891Oy8886Bz2PHjrUXjsMPP9wMHz7c/PTTTyYvL6/F05WVldXiv4noLV++3LRv394km5KSEnuSU+BU13JtvfXWMfs9FcfqhqapJ1YV8WoITauO0dB5x3rbOI5j11s8zgd+Fav9JlXSkcq0fsvKymxpXLzofBjP309EKVkEXJ8DDjjAXH311eb33383Tz31VNB3c+bMsUVKHTt2tDuKgkflFLpULHPMMcfY9/vvv3+giNktknj55ZfNYYcdZoublYux2Wabmeuvv95UVlY2WAcwnD///NOcfvrpplu3bnZ+22yzjS36CvXHH3/YOlD5+fmma9eutjistLQ0qvWiOoDHHnusadu2renUqZO54IIL7AXRS0VwWn/6DaVHQcT999/f4Lx14I8bN87stNNOpl27djade++9t3n33XeDplORhdbnxIkTzYMPPmjXn35nl112MV9++WWt+Wp7Kc1dunSxF+4tttjC/Otf/2rUOgyXDqXvhx9+qLWdlb4999zTrif9rpbr+eefDzsv7WO77rqradWqlenQoYO9C1Wxpmgf0Pzff//9wG94czZ+/fVXu79pf9T/K9futddeC5q/W6/lmWeeMVdddZXNFdO0BQUFtdLiTrtgwQI7H/c33aIiBVVnnHGGXVfa/7fffntbvFrXNrrrrrsC20i5DnXRvqh9UtupTZs29uZL+2yo0DqAeq99rrCwMJBWd5q6to0uNEqXtrOWQcvyz3/+06xZsybot7TudSP4xhtv2ONc2/GBBx6w361du9aWFKh+oZZtwIAB5pZbbgnKtY90X9Wxrtw/d3ncoS4N7RPRpk2/rfqT2icOOeQQs2jRIhvs6rzUq1cvu9wjRowwq1evDrt+tK+q3qXWpY73F198sVaao01T6H4TyflB/6/9R5S75a4bN1e1rnpWoefahvbfhq4BohIlpWHzzTe30+g8sNdee5kZM2aYxlB6zjvvPPPSSy+ZbbfdNnCemj59eq1pv/nmG5uxofN069atzYEHHmg+++yziK9R4WgdaV46V+o6ovda15deemmta5eOxUsuuSSwrXXO1brUPhVumSZPnmyXRdNqedzj96OPPjKjR4+2v6MbOR2j2g+0L51yyin2XKlB1T9C5x3N+dcrtA7g4yFVxbxD6L6k87h+R7+nfUMlFTqWQrnnAk2n8/6HH35oEllK5wDWV9R05ZVX2pPbmWeeacfppKt6RrqAjhkzxp6Epk6dag+IF154wRx55JH24q2dVvUI9f9bbbWV/V/3VTuUDp6LL77Yviq3USc2XYxVXykay5Ytsxd890DSgfL666/bC7Tm51bcV5a2TgILFy60aVPwqXpd+u1oKJDSifKmm26yJxQtoy6aTz75ZGAaBXs6mHUBz8zMNK+++qo599xz7Yl+1KhRdc5b6X344YfNCSecYNe36mCqSE91Mr/44otAxX7X008/bafRSUHLf+utt5qjjjrKBkRu7um3335rLxL6fNZZZ9m0z58/36ZpwoQJUa3DUJpO61DzUYMJrRPvdr777rvtOjjppJPsSUvBl066//d//2dvAFy6SOgCpZOVqiMoh+Hzzz+320YXY12Azj//fLuvuIGrAhY37fo/FXtpu+pkp2BMv6uTnfZHL13QNX+dtBVwhcvNUPq1XArGdPHXidxdXu1HOun98ssvdl2p6sRzzz1nLw46KeuGwEuBmW4QtO51ctdJsS5qcKIT6IknnmiXScvvXU91UVp1QtU+ov1HBg0aVO+20T6j4/C0006z603B7n/+8x974fz444+Dct/nzp1r90n9j/ZLXcy0vvfdd197MdT4TTfd1HzyySe29GDJkiV2m0Wzr2r84sWLbXCgdDekvn0i2rTp4qv9U/NTgKe06TjXTZwugldccYXd3vfcc4/db0JvjObNm2eOO+44c/bZZ5uRI0faba79XBfygw8+uFFpCrffRHJ+0D6q84/qrWrf1zqWxlTjqSsdkVwDRMe09jvt17rIK/1fffWVmTlzZmC9REsBkYJrnU91k6Tz79FHH23P6zr2RenTOU/BnwIj7V+6adFxqxuG3XbbrcFrVF0U6Gl9ax4KsN566y1z++2322BG61wUiOn8o8Bc51BtF91AXXbZZXb7h1bH0HGu9afziao06Bw9a9Ys+532STVC0zlS1xsd5woEte9oH1JdX1Wd0HVTQbGCQlek59+G7LPPPrWOSWUM6UZamRwunWuUaaRjR9t8xYoV9pjR/+u84pZEaJ/VMaBznK4tOgcondq3FDAnJCcFPfbYY7plcL788ss6p2nXrp0zaNCgwOcDDzzQGThwoFNSUhIYV1VV5ey5557O5ptvHhj33HPP2Xm/++67teZZVFRUa9w///lPp1WrVkHzHTlypNOnT5+g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AOXmm282l112mfn+++/jnRQAAICklOaoXDWJdOjQwRQVFZmKigqTnZ1t8vLygr5fvXp1i6WloKDAtGvXzqxbt860bdu2xX4XAAA0XgHXb5Npksxdd90V7yQAAAAktaQLAEeOHBnvJAAAACS1pAsApbKy0rz00kvmp59+sp+32WYbM3z4cJORkRHvpAEAACS8pAsAf/nlFzN06FDz559/mi222MKOu+mmm0zv3r3Na6+9ZjbbbLN4JxEAACChJV0r4NGjR9sgT30Bzpw50w7qHLpfv372OwAAAKRYDuD7779vPvvsM9OxY8fAuE6dOtnuYf7617/GNW0AAADJIOlyAHNycsz69etrjd+wYYPtFgYAAAApFgAefvjh5qyzzjKff/65fTScBuUInn322bYhCAAAAFIsAPz3v/9t6wDuscceJjc31w4q+tUzgukjEAAAIAXrALZv3968/PLLtjWw2w3MVlttZQNAAAAApGAO4HXXXWcfBaeAb9iwYXbQ++LiYvsdAAAAUuxZwOrsecmSJaZr165B41etWmXHqZPolsKzBAEASD4FXL+TLwdQ8WpaWlqt8bNnzw7qGgYAAABJXgewQ4cONvDT8Je//CUoCFSun7qBUUtgAAAApEgAqBa+yv07/fTTzbXXXmuzbl3q/69v3762ZTAAAABSJAAcOXKkfdUj39TtS2Zm0iQdAAAgoSRdHcDCwkLz9ttv1xr/xhtvmNdffz0uaQIAAEgmSRcAjhkzJmxLXxUP6zsAAACkWAA4b948s/XWW9cav+WWW9rOoQEAAJBiAaAaf/z666+1xiv4y8/Pj2pef/75pzn55JNNp06dTF5enhk4cKD56quvYphaAACAxJN0AeCIESPMhRdeaObPnx8U/F1yySVm+PDhEc9nzZo1tjFJVlaWrTv4448/mttvv912NwMAAJDKku5JIOq1e8iQITanrlevXnbcH3/8Yfbee2/z4osv2mcFR0L1BT/++GPz4YcfNjot9CQOAEDyKeD6nXwBoCjJM2bMsE//UNHtdtttZ/bZZ5+o5qF6hIMHD7bB4/vvv2822WQTc+6555ozzzwz4nmwAwEAkHwKuH4nZwAYC7m5ufb14osvNsccc4z58ssvzQUXXGAmTZoU6HMwVGlpqR28O1Dv3r19vQMBAJBsCggAkzMAVF+AyrVbuHChKSsrC/pu9OjR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hgTmvckmmzTLMvlVmpNk3WvrLuKKK64wU6dONatWrar1vVoftZSCggK7k65bt47AEwBSlDIbFixYYPr162efbIHk324FXL+T70kgl19+uXnnnXfM/fffb3JycszDDz9s+wNU9vCTTz4Z7+QBAAAkvKQrAn711VdtoKd6AapPsPfee9t6B3369DGTJ0+m7yAAAIBUywFUNy9uZ9DKtnW7fdlrr73MBx98EOfUAQAAJL6kCwAV/KlMX7bccktbF9DNGVTLIQAAAKRYAKhi39mzZ9v3eqi0nhuoyp0XXXSRueyyy+KdPAAAgISXdHUAFei59HBpPTtw5syZth7gdtttF9e0AQAAJIOkCwBD9e3b1w4AAABI0SJg0SNlDj/8cLPZZpvZQe/16BgAAACkYAB433332Z7B9dDoCy64wA5qDTx06FBbHxAAAAApVgR84403mjvvvDPoodKjR482f/3rX+13o0aNimv6AAAAEl3S5QCuXbvW5gCGOuSQQ+wjXQAAgDFpaWn1Dtdcc03U8/zhhx/M0Ucfbeveax533XVX2OlUIqdp1EvHbrvtZr744otaj2lThk2nTp1M69at7TyXLVtW72/rARAXXnhh0Li7777bPhXsmWeeiXpZ/C7pAsDhw4ebadOm1Rr/8ssv27qAAADAmCVLlgQGBWqqLuUdd+mll0Y9z6KiItsf780332y6d+8edppnn33WXHzxxWb8+PG2l47tt9/eDB482CxfvjyoRw/13/vcc8+Z999/3yxevNgcddRRUaVF87/yyivt9f/444+Peln8LimKgP/9738H3m+99dZmwoQJ5r333jN77LGHHffZZ5+Zjz/+2FxyySWN/g3tzGPHjrV1Cuu6owEAIFl4A7R27drZHLu6grZI7bLLLnZw++IN54477jBnnnmm7bdXJk2aZF577TXz6KOP2v9Rad0jjzxinn76aXPAAQfYaR577DGz1VZb2ev57rvvXm8aHMexVb+eeuopM2PGDLPnnns2aZn8KikCQNX58+rQoYP58ccf7eDSU0C0c1111VVRz//LL780DzzwAP0IAgAioiCkuKI4Lr+dl5lng7lYURFsfU4++WQbxEWirKzMfP311zZDxZWenm777f3000/tZ31fXl5ux7n0ZK9NN93UTlNfAFhRUWHT884779icQ67bKR4Auo9+aw4bNmwwJ510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zeuvv+6kpaU5f/75p9NSYpWOHXbYwTn99NODxkVy0kjkZVQAeMEFF9R7gkxPTw+6UN1///32QlZaWuok2zacOnWqPTmXl5fHfRvuuuuuzqhRowKfKysrnZ49ezo33XRT2OmPPfZY57DDDgsat9tuuzn//Oc/Iz4mE3n5Qunmo02bNs4TTzwRFDyMGDHCSQTRLl9D59ZE236x2IZ33nmn3YYbNmxIyG3oFcl54PLLL3e22WaboHHHHXecM3jw4JitMz+jCDhJLFiwwCxdutQWUXifY6js7k8//dR+1quK6nbeeefANJpezyz+/PPPWyytsUjH119/bWbNmmWLHUONGjXKdO7c2ey6667m0UcftcU4La0pyzh58mSb/m233daMHTvWFBUVBc1XRY3dunULjBs8eLB9KPoPP/xgWkqs9iUV/6oIOTMzM67bsKyszO5T3uNHy6LP7vETSuO907vbwp0+kmOypTRm+UJpPywvLzcdO3asVQSnqggq2j/nnHPMqlWrTEtr7PKpykKfPn1M7969zYgRI4KOoUTafrHaho888og5/vjjTX5+fsJtw8Zo6BiMxTrzs+CzMhKWTlTiDQzcz+53etVB7qULr07o7jQtldampkMnsq222srWk/S67rrrzAEHHGDrx7355pvm3HPPtSd51TVrSY1dxhNPPNFekFQH5ttvvzVXXHGFmTt3rnnxxRcD8w23jd3vkmkbrly50lx//fXmrLPOivs2VFoqKyvDrts5c+aE/Z+6toX3eHPH1TVNS2nM8oXSvqj90nsxVV2xo446yvTr18/Mnz/fXHnllebQQw+1F9eMjAyTyMunYEc3F9ttt529EZk4caI9nygI7NWrV0Jtv1hsQ9V7+/777+250ytRtmFj1HUM6oa4uLjYrFmzpsn7vZ8RAMbQmDFjzC233FLvND/99JOtVJ7Ky9dUOrCffvppc/XVV9f6zjtu0KBBprCw0Nx2220xCx6aexm9wZBy+lT5/MADD7Qn5s0228ykyjbUCVoV0bfeemtzzTXXtOg2RPRuvvlm2xBHOUXehhLKTfLurwqmtJ9qOu23iWyPPfawg0vBn24qH3jgAXtjkmoU+GkbKVfdK5m3IZoXAWAMXXLJJbbFVX369+/fqHl3797dvi5btswGDS593mGHHQLTLF++POj/1HpUrTPd/2+J5WtqOp5//nlbHHXKKac0OK2Ka3QyLy0tjcnzIltqGb3pl19++cWelPW/oS3YtI0lWbbh+vXrba5DmzZtzLRp00xWVlaLbsNwVNys3A53Xbr0ua7l0fj6po/kmGwpjVk+l3LGFAC+9dZbNjhoaN/Qb2l/bcngoSnL59J+qBsOpT3Rtl9Tl1E3UQrglbvekHhtw8ao6xhUtRK12tb6aup+4WvxroTod9E2Apk4cWJgnFqPhmsE8tVXXwWmeeONN+LWCKSx6VBDidCWo3W54YYbnA4dOjgtLVbr+qOPPrLzUQtEbyMQbwu2Bx54wDYCKSkpcRJ9+bRP7r777nYbFhYWJtQ2VGXx8847L6iy+CabbFJvI5DDDz88aNwee+xRqxFIfcdkS4p2+eSWW26x+9ann34a0W8sWrTI7gMvv/yykwzLF9rIZYsttnAuuuiihNx+TVlGXUeU7pUrVyb0NmxMIxD1iuClnghCG4E0Zb/wMwLAOPn9999t9wtuVyd6r8Hb5YlOVmou7+2yQM3bdeB+++23tmVXuG5gBg0a5Hz++ec2uFA3HPHqBqa+dKirCS2fvveaN2+ePTmpxWkodS/y0EMP2W44NN19991nuwhQlzrxEO0yqmuU6667zgZVCxYssNuxf//+zj777FOrG5hDDjnEmTVrlu3uoEuXLnHrBiaa5dPFU61kBw4caJfV2+2Elive21DdRegi+fjjj9sA96yzzrLHk9vi+u9//7szZsyYoG5gMjMzbYCgblLGjx8fthuYho7JlhLt8intaqH9/PPPB20r9xyk10svvdQGh9pf33rrLWfHHXe0+0FL3ow0dvl0btVNy/z5852vv/7aOf74453c3FzbVUgibr/GLKNrr732sq1jQyXaNlR63GudAkB1hab3uh6Klk3LGNoNzGWXXWaPQXVbFK4bmPrWGepGABgnapqvAyB0ePfdd2v1l+bSHevVV1/tdOvWze7wBx54oDN37txa/ULpIq2gUnf2p512WlBQ2VIaSodORqHLKwp0evfube/iQikoVNcwmmd+fr7to27SpElhp03EZVy4cKEN9jp27Gi3n/rV04nN2w+g/Pbbb86hhx7q5OXl2T4AL7nkkqBuVBJ1+fQabp/WoGkTYRuqH7FNN93UBj7KOVAfhy7lWuq4DO3G5i9/+YudXt1RvPbaa0HfR3JMtqRolq9Pnz5ht5UCXSkqKrI3IroBUeCr6dXHWjwvrNEs34UXXhiYVttn6NChzsyZMxN6+zVmH50zZ47dbm+++WateSXaNqzrHOEuk161jKH/o3OG1odumL3XxEjWGeqWpj/xLoYGAABAy6EfQAAAAJ8hAAQAAPAZAkAAAACfIQAEAADwGQJAAAAAnyEABAAA8BkCQAAAAJ8hAASAGFq6dKk5+OCDTX5+vmnfvn2z/Mbf//53c+ONN5pEMH36dPvs3KqqqngnBUAUCAABHzn11FNNWlparWHIkCEmWe23337mwgsvNInizjvvNEuWLDGzZs0yP//8c8znP3v2bPO///3PjB492jSngQMHmrPPPjvsd//9739NTk6OWblypd13srKyzOTJk5s1PQBiiwAQ8BldsBWgeIcpU6Y062+WlZWZeNIDjyoqKlrkt+bPn2922mkns/nmm5uuXbvGfH3dc8895phjjjGtW7c2zemMM84wzzzzjCkuLq713WOPPWaGDx9uOnfuHLix+Pe//92s6QEQWwSAgM8o56Z79+5BQ4cOHQLfK0fw4YcfNkceeaRp1aqVDWReeeWVoHl8//335tBDD7VBSLdu3WyRpHKDvLly5513ns2ZU5AwePBgO17z0fxyc3PN/vvvb5544gn7e2vXrjWFhYWmbdu25vnnnw/6rZdeeskWp65fv77WsijweP/9983dd98dyM387bffzHvvvWffv/766zYY0zJ/9NFHNjgbMWKETbPSvssuu5i33noraJ59+/a1xaunn366adOmjdl0003Ngw8+GBScadl69Ohhl6NPnz7mpptuCvzvCy+8YJ588kn7+0qfaPn+8Y9/mC5duthlPOCAA2xOnuuaa66xxaha7/369bPzDaeystKun2HDhtVK8w033GBOOeUUu1xKk9b1ihUr7PJq3HbbbWe++uqroP/TOtl7771NXl6e6d27t81V1HaQk08+2QZ/Wh6vBQsW2PWrANGl9GjeWr8AkkQ9zwkGkGL0sPURI0bUO41OC7169XKefvppZ968ec7o0aOd1q1bO6tWrbLfr1mzxj5cfuzYsc5PP/3kzJw50zn44IOd/fffPzAPPdBd/3PZZZfZh9Vr+PXXX+0D6S+99FL7ecqUKc4mm2xif0/zFD2ofujQoUHpGT58uHPKKaeETevatWudPfbYw/7fkiVL7FBRURF46Px2223nvPnmm84vv/xi0z9r1ixn0qRJznfffef8/PPPzlVXXeXk5uY6v//+e2Ceffr0cTp27Ojce++9dvlvuukmJz093aZZbrvtNqd3797OBx984Pz222/Ohx9+aNeVLF++3BkyZIhz7LHH2rQofXLQQQc5w4YNc7788kv7u5dcconTqVOnwDodP368k5+fb/9X63P27Nlhl1ffabmWLl0aNN5Ns5ZN8z/nnHOctm3b2vlNnTrVmTt3rnPEEUc4W221lVNVVWX/R+tEv3nnnXfa//n444+dQYMGOaeeempgvsccc0zQdpVx48bZ5a+srAwa361bN+exxx6rY68CkGgIAAGfBYAZGRn2wu8dJkyYEJhGAYYCI9eGDRvsuNdff91+vv76651DDjkkaL6LFi2y0yjQcANABRNeV1xxhbPtttsGjfvXv/4VFAB+/vnnNn2LFy+2n5ctW+ZkZmY67733Xp3LpN+64IILgsa5AeBLL73U4DrZZpttnHvuuScomDr55JMDnxUwde3a1bn//vvt5/PPP9854IADAoFUKAXYWs8uBYgKxkpKSoKm22yzzZwHHnggEAAqOFYAWZ9p06bZ9RP626FpVvCp5b/66qsD4z799FM7Tt/JGWec4Zx11llB81FaFewWFxfbz9OnT3fS0tJs8O6uC/2Wd/9waXtfc8019aYfQOKgCBjwGRW9qoGCdwit7K/iQpeKX1VsuXz5cvtZRZfvvvuuLVZ0hy233NJ+5y0CVNGr19y5c22Rq9euu+5a6/M222xji4blqaeessWZ++yzT6OWdeeddw76vGHDBnPppZearbbayrbQVdp/+ukns3DhwjqXX0W5KiZ3l1/FulpnW2yxhS0yffPNN+tNg9aXfrdTp05B60xFqd71peVUEXF9VCSr4mylKZQ3zSridhtyhI7zbsfHH388KE0qqldrXqVN1Jq5V69ets6fvP3223ZdnXbaabV+X8XIRUVF9aYfQOLIjHcCALQsBXQDBgyodxq16vRSwOF286FgRnW+brnlllr/p3px3t9pDNWVu/fee82YMWNs4KFgI1zAE4nQNCj4mzFjhpk4caJdBwpa/va3v9VqdFHf8u+44442QFL9QtUfPPbYY81BBx1Uq+6iS+tL60X15kJ5u4mJZH2pPqWCLKU3Ozu7zjS76yvcOO92/Oc//xm2NbHqPUp6eroNeBWQq56itoduIPr371/rf1avXt1gAAsgcRAAAoiKAiA1DFDDg8zMyE8hyjFT9yVeX375Za3p1Pjg8ssvt61Kf/zxRzNy5Mh656tASI0jIvHxxx/bgEYNXNwgSI1GoqUc0eOOO84OCiDVsloBUMeOHcOuL/UNqHWlddYUaigiWi/u+8ZSujSfhm4GFICrgcmLL75opk2bZhuqhCopKbG5mYMGDWpSmgC0HIqAAZ8pLS21AYl38LbgbcioUaNssHPCCSfYAE4X/jfeeMMGCvUFYsptmjNnjrniiits/3hTp061RZDizeFTi+SjjjrKXHbZZeaQQw6xRZD1UVD1+eef20BOy1Ffh8RqgaxARkW4KgI98cQTo+7A+I477rDd5mhZtBzPPfecLSKuq9Nn5Q7uscce5ogjjrDFxUrnJ598Yv71r3/VapXbEOWwKXBT692m0nZQOtSiWetj3rx55uWXX7afvdQqWa2WzzrrLFv8rG0T6rPPPrPfaTkBJAcCQMBn9OQGFUl6h7322ivi/+/Zs6fNSVOwpwBN9czU3YsCIBUZ1kWBhIpJFYCpvtr9999vgyBR8OClLkZUzKmuWBqiYt2MjAyz9dZb2wAptD5faPCmAHPPPfe0xdiq86aAKhrqGubWW2+19QtVp1EBnXI261p2Bbf6XvUYFST/5S9/Mccff7z5/fffA/Xyoi0ij0Wny9oG6kJHQay6glHu3bhx4+z2DaXtsWbNGhswh+uiRgHxSSedZLsNApAc0tQSJN6JAOBPEyZMMJMmTTKLFi2q9aSJiy66yCxevLhWXTe/U0MQFac/++yzCZHjplxXpUe5mQryASQH6gACaDH33XefzTVTi1jlIt52221BRY5q4KAnk9x88822yJjgrzY1XFFH09EU2zcn5YBquxL8AcmFHEAALUa5esq5Uh1CtTTVE0TGjh0baEyilqbKFVRxqeqjNffjzgDArwgAAQAAfIZGIAAAAD5DAAgAAOAzBIAAAAA+QwAIAADgMwSAAAAAPkMACAAA4DMEgAAAAD5DAAgAAOAzBIAAAADGX/4fCJ0j4Wf4CTIAAAAASUVORK5CYII=",
- "text/html": [
- "\n",
- " \n",
- "
\n",
- " Figure\n",
- "
\n",
- "

\n",
- "
\n",
- " "
- ],
- "text/plain": [
- "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"\n",
"temperatures=[1, 10, 100]\n",
@@ -119,36 +67,10 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": null,
"id": "ea1f36ac",
"metadata": {},
- "outputs": [
- {
- "data": {
- "application/vnd.jupyter.widget-view+json": {
- "model_id": "309863fb77bf4e798eecf4ceb72a9e96",
- "version_major": 2,
- "version_minor": 0
- },
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjYsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvq6yFwwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAZQVJREFUeJzt3Qd8FGX+x/EnPSH0DoIU8ayo2PXsDUQBy9k9sZyeimJX8BRsWLGdp2IvJ6JYsPw9Uey9IlhBRBSUXgPpZf6v75PMMrvZJLvJJlvm8369Jrs7O5l9pv/maZPmOI5jAAAA4Bvp8U4AAAAAWhYBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDPEADGwamnnmr69u0bNC4tLc1cc801MfuN9957z85Tr/XRb2q6lStXxuy399tvPzsku4qKCnP55Zeb3r17m/T0dHPEEUeYVLBhwwbzj3/8w3Tv3t1u+wsvvDAu6Qjd5x9//HE77rfffgua7rbbbjP9+/c3GRkZZocddkjpbQMgNnSN1bU22mtirCXy9TAlA0D3QuIOubm5pmfPnmbw4MHm3//+t1m/fn2j5/3jjz/ai1boRQqp59FHH7XBx9/+9jfzxBNPmIsuuijmv3HffffZ/bUl3XjjjfY3zznnHPPf//7X/P3vfzeJ6s0337SB3l//+lfz2GOP2bS31LaJlf/9739R3dzFY59IBkVFRXY9tvQFPNlwjUKkMk0Ku+6660y/fv1MeXm5Wbp0qT1xKLfjjjvuMK+88orZbrvtGnVwXXvttTaiD83Fi9RDDz1kqqqqGvW/aDnvvPOO2WSTTcydd97ZbL+hi33nzp2D7lRbYrl23313M378eJNIFIgef/zxJicnJyityuF75JFHTHZ2dotum1gGgPfee2/EQWA89olkCQB17pVEzVFJBLG4RqWiffbZxxQXFwedR/wupQPAQw891Oy8886Bz2PHjrUXjsMPP9wMHz7c/PTTTyYvL6/F05WVldXiv4noLV++3LRv394km5KSEnuSU+BU13JtvfXWMfs9FcfqhqapJ1YV8WoITauO0dB5x3rbOI5j11s8zgd+Fav9JlXSkcq0fsvKymxpXLzofBjP309EKVkEXJ8DDjjAXH311eb33383Tz31VNB3c+bMsUVKHTt2tDuKgkflFLpULHPMMcfY9/vvv3+giNktknj55ZfNYYcdZoublYux2Wabmeuvv95UVlY2WAcwnD///NOcfvrpplu3bnZ+22yzjS36CvXHH3/YOlD5+fmma9eutjistLQ0qvWiOoDHHnusadu2renUqZO54IIL7AXRS0VwWn/6DaVHQcT999/f4Lx14I8bN87stNNOpl27djade++9t3n33XeDplORhdbnxIkTzYMPPmjXn35nl112MV9++WWt+Wp7Kc1dunSxF+4tttjC/Otf/2rUOgyXDqXvhx9+qLWdlb4999zTrif9rpbr+eefDzsv7WO77rqradWqlenQoYO9C1Wxpmgf0Pzff//9wG94czZ+/fVXu79pf9T/K9futddeC5q/W6/lmWeeMVdddZXNFdO0BQUFtdLiTrtgwQI7H/c33aIiBVVnnHGGXVfa/7fffntbvFrXNrrrrrsC20i5DnXRvqh9UtupTZs29uZL+2yo0DqAeq99rrCwMJBWd5q6to0uNEqXtrOWQcvyz3/+06xZsybot7TudSP4xhtv2ONc2/GBBx6w361du9aWFKh+oZZtwIAB5pZbbgnKtY90X9Wxrtw/d3ncoS4N7RPRpk2/rfqT2icOOeQQs2jRIhvs6rzUq1cvu9wjRowwq1evDrt+tK+q3qXWpY73F198sVaao01T6H4TyflB/6/9R5S75a4bN1e1rnpWoefahvbfhq4BohIlpWHzzTe30+g8sNdee5kZM2aYxlB6zjvvPPPSSy+ZbbfdNnCemj59eq1pv/nmG5uxofN069atzYEHHmg+++yziK9R4WgdaV46V+o6ovda15deemmta5eOxUsuuSSwrXXO1brUPhVumSZPnmyXRdNqedzj96OPPjKjR4+2v6MbOR2j2g+0L51yyin2XKlB1T9C5x3N+dcrtA7g4yFVxbxD6L6k87h+R7+nfUMlFTqWQrnnAk2n8/6HH35oEllK5wDWV9R05ZVX2pPbmWeeacfppKt6RrqAjhkzxp6Epk6dag+IF154wRx55JH24q2dVvUI9f9bbbWV/V/3VTuUDp6LL77Yviq3USc2XYxVXykay5Ytsxd890DSgfL666/bC7Tm51bcV5a2TgILFy60aVPwqXpd+u1oKJDSifKmm26yJxQtoy6aTz75ZGAaBXs6mHUBz8zMNK+++qo599xz7Yl+1KhRdc5b6X344YfNCSecYNe36mCqSE91Mr/44otAxX7X008/bafRSUHLf+utt5qjjjrKBkRu7um3335rLxL6fNZZZ9m0z58/36ZpwoQJUa3DUJpO61DzUYMJrRPvdr777rvtOjjppJPsSUvBl066//d//2dvAFy6SOgCpZOVqiMoh+Hzzz+320YXY12Azj//fLuvuIGrAhY37fo/FXtpu+pkp2BMv6uTnfZHL13QNX+dtBVwhcvNUPq1XArGdPHXidxdXu1HOun98ssvdl2p6sRzzz1nLw46KeuGwEuBmW4QtO51ctdJsS5qcKIT6IknnmiXScvvXU91UVp1QtU+ov1HBg0aVO+20T6j4/C0006z603B7n/+8x974fz444+Dct/nzp1r90n9j/ZLXcy0vvfdd197MdT4TTfd1HzyySe29GDJkiV2m0Wzr2r84sWLbXCgdDekvn0i2rTp4qv9U/NTgKe06TjXTZwugldccYXd3vfcc4/db0JvjObNm2eOO+44c/bZZ5uRI0faba79XBfygw8+uFFpCrffRHJ+0D6q84/qrWrf1zqWxlTjqSsdkVwDRMe09jvt17rIK/1fffWVmTlzZmC9REsBkYJrnU91k6Tz79FHH23P6zr2RenTOU/BnwIj7V+6adFxqxuG3XbbrcFrVF0U6Gl9ax4KsN566y1z++2322BG61wUiOn8o8Bc51BtF91AXXbZZXb7h1bH0HGu9afziao06Bw9a9Ys+532STVC0zlS1xsd5woEte9oH1JdX1Wd0HVTQbGCQlek59+G7LPPPrWOSWUM6UZamRwunWuUaaRjR9t8xYoV9pjR/+u84pZEaJ/VMaBznK4tOgcondq3FDAnJCcFPfbYY7plcL788ss6p2nXrp0zaNCgwOcDDzzQGThwoFNSUhIYV1VV5ey5557O5ptvHhj33HPP2Xm/++67teZZVFRUa9w///lPp1WrVkHzHTlypNOnT5+g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AOXmm282l112mfn+++/jnRQAAICklOaoXDWJdOjQwRQVFZmKigqTnZ1t8vLygr5fvXp1i6WloKDAtGvXzqxbt860bdu2xX4XAAA0XgHXb5Npksxdd90V7yQAAAAktaQLAEeOHBnvJAAAACS1pAsApbKy0rz00kvmp59+sp+32WYbM3z4cJORkRHvpAEAACS8pAsAf/nlFzN06FDz559/mi222MKOu+mmm0zv3r3Na6+9ZjbbbLN4JxEAACChJV0r4NGjR9sgT30Bzpw50w7qHLpfv372OwAAAKRYDuD7779vPvvsM9OxY8fAuE6dOtnuYf7617/GNW0AAADJIOlyAHNycsz69etrjd+wYYPtFgYAAAApFgAefvjh5qyzzjKff/65fTScBuUInn322bYhCAAAAFIsAPz3v/9t6wDuscceJjc31w4q+tUzgukjEAAAIAXrALZv3968/PLLtjWw2w3MVlttZQNAAAAApGAO4HXXXWcfBaeAb9iwYXbQ++LiYvsdAAAAUuxZwOrsecmSJaZr165B41etWmXHqZPolsKzBAEASD4FXL+TLwdQ8WpaWlqt8bNnzw7qGgYAAABJXgewQ4cONvDT8Je//CUoCFSun7qBUUtgAAAApEgAqBa+yv07/fTTzbXXXmuzbl3q/69v3762ZTAAAABSJAAcOXKkfdUj39TtS2Zm0iQdAAAgoSRdHcDCwkLz9ttv1xr/xhtvmNdffz0uaQIAAEgmSRcAjhkzJmxLXxUP6zsAAACkWAA4b948s/XWW9cav+WWW9rOoQEAAJBiAaAaf/z666+1xiv4y8/Pj2pef/75pzn55JNNp06dTF5enhk4cKD56quvYphaAACAxJN0AeCIESPMhRdeaObPnx8U/F1yySVm+PDhEc9nzZo1tjFJVlaWrTv4448/mttvv912NwMAAJDKku5JIOq1e8iQITanrlevXnbcH3/8Yfbee2/z4osv2mcFR0L1BT/++GPz4YcfNjot9CQOAEDyKeD6nXwBoCjJM2bMsE//UNHtdtttZ/bZZ5+o5qF6hIMHD7bB4/vvv2822WQTc+6555ozzzwz4nmwAwEAkHwKuH4nZwAYC7m5ufb14osvNsccc4z58ssvzQUXXGAmTZoU6HMwVGlpqR28O1Dv3r19vQMBAJBsCggAkzMAVF+AyrVbuHChKSsrC/pu9OjR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hgTmvckmmzTLMvlVmpNk3WvrLuKKK64wU6dONatWrar1vVoftZSCggK7k65bt47AEwBSlDIbFixYYPr162efbIHk324FXL+T70kgl19+uXnnnXfM/fffb3JycszDDz9s+wNU9vCTTz4Z7+QBAAAkvKQrAn711VdtoKd6AapPsPfee9t6B3369DGTJ0+m7yAAAIBUywFUNy9uZ9DKtnW7fdlrr73MBx98EOfUAQAAJL6kCwAV/KlMX7bccktbF9DNGVTLIQAAAKRYAKhi39mzZ9v3eqi0nhuoyp0XXXSRueyyy+KdPAAAgISXdHUAFei59HBpPTtw5syZth7gdtttF9e0AQAAJIOkCwBD9e3b1w4AAABI0SJg0SNlDj/8cLPZZpvZQe/16BgAAACkYAB433332Z7B9dDoCy64wA5qDTx06FBbHxAAAAApVgR84403mjvvvDPoodKjR482f/3rX+13o0aNimv6AAAAEl3S5QCuXbvW5gCGOuSQQ+wjXQAAgDFpaWn1Dtdcc03U8/zhhx/M0Ucfbeveax533XVX2OlUIqdp1EvHbrvtZr744otaj2lThk2nTp1M69at7TyXLVtW72/rARAXXnhh0Li7777bPhXsmWeeiXpZ/C7pAsDhw4ebadOm1Rr/8ssv27qAAADAmCVLlgQGBWqqLuUdd+mll0Y9z6KiItsf780332y6d+8edppnn33WXHzxxWb8+PG2l47tt9/eDB482CxfvjyoRw/13/vcc8+Z999/3yxevNgcddRRUaVF87/yyivt9f/444+Peln8LimKgP/9738H3m+99dZmwoQJ5r333jN77LGHHffZZ5+Zjz/+2FxyySWN/g3tzGPHjrV1Cuu6owEAIFl4A7R27drZHLu6grZI7bLLLnZw++IN54477jBnnnmm7bdXJk2aZF577TXz6KOP2v9Rad0jjzxinn76aXPAAQfYaR577DGz1VZb2ev57rvvXm8aHMexVb+eeuopM2PGDLPnnns2aZn8KikCQNX58+rQoYP58ccf7eDSU0C0c1111VVRz//LL780DzzwAP0IAgAioiCkuKI4Lr+dl5lng7lYURFsfU4++WQbxEWirKzMfP311zZDxZWenm777f3000/tZ31fXl5ux7n0ZK9NN93UTlNfAFhRUWHT884779icQ67bKR4Auo9+aw4bNmwwJ510knnooYfMDTfc0Gy/AwBIHQr+dnt6t7j89ucnfm5aZbWK2fxmzZpV7/cqOo7UypUrTWVlpenWrVvQeH2eM2eOfb906VKTnZ1d6/Gtmkbf1UfXatETwRQ0IsUDwOakSqiHHXaYvRNpKAAsLS21g6ugoKAFUggAQPPRk7SSxV577WUD1quvvtpMmTLFZGb6PoxpNF+vObUaUgVVFQFH4qabbjLXXntts6cLAJDYVAyrnLh4/XYsxbIIuHPnziYjI6NWi159dusf6lVFxerVw5sL6J2mLgMHDjS33367zbQ57rjjbIMTgsDG8e1aW7RokW3woQqkaqYeCdVpUMsmbw5g7969mzGVAIBEpDp4sSyGjadYFgGraHennXayT+w64ogj7Liqqir72e2/V99nZWXZcer+RebOnWsWLlwYaNxZnx122MH+r4LAY4891gaBmh+i49sAUJVQ1SR9xx13DIxTvYUPPvjA/Oc//7FFvbqL8VJfQxoAAPBjEbBy7twGmHr/559/2gBSuYjufJRRMnLkSLPzzjubXXfd1fasUVhYGGgVrBbJZ5xxhp2uY8eONsA8//zzbfDXUAtgl7qWUUOQAw880AaBU6dOJQiMkm8DQO003333XdA47ZyqVHrFFVfUCv4AAPA79dc3aNCgwOeJEyfaYd9997Xds4mKZlesWGHGjRtnG3Uox2769OlBDUPUu4daBysHUBku6idQj3qNhoqD3SDwmGOOsUGgciARmTRHbdkT3LfffhvxtE1pEq5exrWjRtoPoIqAdSejPo2iySIHACQPPbVCvVH069cv4ipDSOztVsD1OzlyABWUqb6FYtWG+j5SMS4AAABSqB/Ab775xj6+5rLLLgtUFlXHkWoVdOuttzbpd9zsawAAgFSWFAFgnz59Au9Vzq9Hww0dOjSo2FetcdUvkNvqCAAAAOGlmySjhhsqzw+lcd5HwwEAACBFAkA9LFodMqv5uUvvNU7fAQAAIAWKgL3UG/mwYcNMr169Ai1+1UpYjUNeffXVeCcPAJCikqDTDHiwvVIsAFSnkr/++quZPHly4MHS6nPoxBNPNPn5+fFOHgAgxbgdDBcVFZm8vNg+hg3NR9tL6CA6RQJAUaB31llnxTsZAAAf0IMB9MxaPT1KWrVq1WCXZIhvzp+CP20vbTce7JBCAeB///tf88ADD9icQHUBo1bC6lW8f//+ZsSIEfFOHgAgxXTv3t2+ukEgEp+CP3e7IQUCwPvvv98+XubCCy80N9xwQ6Dj5w4dOtgneBAAAgBiTTl+PXr0MF27djXl5eXxTg4aoGJfcv5S4FFwXltvvbW58cYbbX9/bdq0MbNnz7Y5f99//719lNvKlStbLC08SgYAgORTwPU7+bqB0VNBvA+iduXk5JjCwsK4pAkAACCZJF0AqA6fZ82aVWv89OnT6QcQAAAgFesAXnzxxWbUqFGmpKTEtvT54osvzJQpU2xH0A8//HC8kwcAAJDwki4A/Mc//mH7YbrqqqtsM2/1/9ezZ09z9913m+OPPz7eyQMAAEh4SdcIxEsB4IYNG2yrrHigEikAAMmngOt38uUAeqkzTg0AAABIsQBQrX4j7XV95syZzZ4eAACAZJYUAaD6/AMAAEBsJHUdwHijDgEAAMmngOt38vUDCAAAAB8UAXfs2NH8/PPPpnPnzvaZv/XVB1y9enWLpg0AACDZJEUAeOedd9rn/spdd90V7+QAAAAkNeoANgF1CAAASD4FXL+TIwewLnocXFlZWdA4v25IAACAlG0EUlhYaM477zz79I/8/HxbJ9A7AAAAIMUCwMsvv9y888475v777zc5OTnm4YcfNtdee619HvCTTz4Z7+QBAAAkvKQrAn711VdtoLfffvuZ0047zey9995mwIABpk+fPmby5MnmpJNOincSAQAAElrS5QCqm5f+/fsH6vu53b7stdde5oMPPohz6gAAABJf0gWACv4WLFhg32+55ZZm6tSpgZzB9u3bxzl1AAAAiS/pAkAV+86ePdu+HzNmjLn33ntNbm6uueiii8xll10W7+QBAAAkvKTvB/D33383X3/9ta0HuN1227Xob9OPEAAAyaeA63fy5QCqAUhpaWngsxp/HHXUUbY4mFbAAAAAKZgDmJGRYZYsWWL7AfRatWqVHVdZWdliaeEOAgCA5FPA9Tv5cgAVr6alpdUa/8cff9iNCQAAgBTpB3DQoEE28NNw4IEHmszMjUlXrp9aBg8ZMiSuaQQAAEgGSRMAHnHEEfZ11qxZZvDgwaZ169aB77Kzs03fvn3N0UcfHccUAgAAJIekCQDHjx9vXxXoHXfccbbrFwAAAPigDuDIkSNNSUmJfQbw2LFjA08CmTlzpvnzzz/jnTwAAICElzQ5gK5vv/3WHHTQQbbBx2+//WbOPPNM07FjR/Piiy+ahQsX0hUMAABAquUA6okfp556qpk3b15QMfDQoUN5FjAAAEAq5gB+9dVX5sEHH6w1fpNNNjFLly6NS5oAAACSSdLlAObk5NgOHEP9/PPPpkuXLnFJEwAAQDJJugBw+PDh5rrrrjPl5eX2s/oFVN2/K664gm5gAAAAUjEAvP32282GDRvsY9+Ki4vNvvvuawYMGGDatGljJkyYEO/kAQAAJLykqwOo1r8zZswwH330kW0RrGBwxx13tC2DAQAA0LA0Rw/XRaPwMGkAAJJPAdfv5MoBrKqqMo8//rjt8099AKr+X79+/czf/vY38/e//91+BgAAQIrUAVRGpRqA/OMf/7BP/Bg4cKDZZpttzO+//277BTzyyCPjnUQAAICkkDQ5gMr5U0fPb7/9ttl///2DvnvnnXfMEUccYZ8Ccsopp8QtjQAAAMkgaXIAp0yZYq688spawZ8ccMABZsyYMWby5MlxSRsAAEAySZoAUC1+hwwZUuf3hx56qJk9e3aLpgkAACAZJU0AuHr1atOtW7c6v9d3a9asadE0AQAAJKOkCQArKytNZmbdVRYzMjJMRUVFi6YJAAAgGWUmUytgtfbVs4DDKS0tbfE0AQAAJKOkCQBHjhzZ4DS0AAYAAEihAPCxxx6LdxIAAABSQtLUAQQAAEBsEAACAAD4DAEgAACAzxAAAgAA+AwBIAAAgM8QAAIAAPgMASAAAIDP+DoAvOmmm8wuu+xi2rRpY7p27WqOOOIIM3fu3HgnCwAAoFn5OgB8//33zahRo8xnn31mZsyYYcrLy80hhxxiCgsL4500AACAZpPm6CG7sFasWGFzAhUY7rPPPg1OX1BQYNq1a2fWrVtn2rZt2yJpBAAATVPA9dvfOYChtCNIx44d450UAACAZpM0zwJublVVVebCCy80f/3rX822224bdprS0lI7eO8gAAAAkg05gDVUF/D77783zzzzTL2NRpRl7A69e/du0TQCAADEAnUAjTHnnXeeefnll80HH3xg+vXrV+d04XIAFQT6uQ4BAADJpoA6gP4uAlbse/7555tp06aZ9957r97gT3JycuwAAACQzDL9Xuz79NNP29w/9QW4dOlSO153BXl5efFOHgAAQLPwdRFwWlpa2PGPPfaYOfXUUxv8f7KQAQBIPgVcv/2dA+jj2BcAAPgYrYABAAB8hgAQAADAZwgAAQAAfIYAEAAAwGcIAAEAAHyGABAAAMBnCAABAAB8hgAQAADAZwgAAQAAfIYAEAAAwGcIAAEAAHyGABAAAMBnMuOdAAAAgCClG4wpWGxMwR/GdOhrTMf+8U5RyiEABAAAcQju/qwZat6vc9//YUzJuo3THzjemL0vjmeKUxIBIAAAiI2SAk9wt3hjQBd4/2dwcFefnLbGtN3EmOzWzZ1qXyIABAAA9XMcY4pWG7PeE8h5gzr7usSYsvVRBHc9qwM897Wd+75X9Wtu2+ZeKl8jAAQAwM8qK4wpXO4J6BZ7Aj3PUFka2fxy29cO7tr2qHmtGUdwF3cEgAAApKqywuqcORvQeV6Va7der0uM2bDUGKcqsvnldzGmTQ9PcOcdasZl5zf3UiEGCAABAEg2VVXGFK6oHdjZoG7xxuCuNML6dmkZNYGdhp7GtAkN7jSuhzGZOc29ZGghBIAAACRaQ4r1S4ODOndwP29YZkxVRWTzUyMKN7jzBnZ2XM175eylZzT3kiGBEAACANASykuqi1ttcKdAbmlNbp37uWZc2YbI5peWbkx+V09gp1c3B8/zSn07hEEACABAU1SWV+fIeQO7cK/FayKfZ047Y9p0rx6CArruG4M9BX8ZXMbROOw5AADUG9hp8BS9BoK6mvdFKyOfZ0bOxpw6N5izrzVFtK0V8PWgIQWaHQEgAMBfKkqrAzg31y7wujQksFulDvAim2d61sYcOw0K5NzAzvua18GYtLTmXkKgQQSAAIDU6Ki4dH11MFcrsHPH6XVpdEWx6ZnGtO4WHMQFBXf6rocxeR2NSU9vziUEYooAEACQuKoqq3PiggI5vV9ek2NXM05DeVHk883I9gRy3Tzv3QCPwA6pjQAQANDyuXVq6WqDODeA87wPBHXLq/u6cyojn3d2m5qArmawAZ372rU6qNNnimLhcwSAAIDYdXOiR4ptWLExmFMAVyvIWx5dbp1Jq+6nzgZynuAu8NkN7rrTeAKIEAEgAKBuFWU1QV1NbpwbxAUCu5pXTVMS4VMnvB0UB4K5rjWvCvTcXLua71p1prsTIMY4ogDAlzl1Kzbm1tUK8PS+5rVkbXTztnXrum3MsbOBXU1wZ58jW5Nbpz7sclo31xIiiVVUVpnfVxeZecs2mF+Wrzd/HdDZDNq0Q7yTlXIIAAEgVVrA2qBuxcYArnDlxsAuMH6FMaUF0c1fLWEVsCl3zr6GBHXeQC+3PXXrEJHSikrz28oiM2/5+ppgb4N9v2BloSmv3Nj9TkWVQwDYDAgAASBROyFW61c3eFMwFwjiat4Xet5XlEQ3f/VbZ3PiumzMkQsN8PI9QR0tYdFIxWWVZv6KjQGeDfZWbDC/ryoylVXh+1nMy8owA7q2Npt3bW227sGj7JoDASAAtISqquri1EDwVjOEC/I0RNNXnbdOnQI6d/AGdPmdg9+TU4cYW19SXhPkVQd7bsD3x5pim0kdTpvczECgt3nXNtXvu7U2PdvlmfR09s/mRAAIAE0N6PQoMO9r4L2CuVUbA71oujORtPTqBhA2ePMEdvrs5t4pqHO/z27VXEsLBKwpLLM5eN5iW70uWVd3LnSHVllm825tbKA3wBPsdWubY9K4EYkLAkAAkMqK6ly30GBOgVsgl67ms/sabUAnue02BnKtOm3MmXMDO+9n9VVH0SviwHEcs2JDqfmlprhWwZ4b6K3cUFbn/3Vtk2Nz8AZ0aR0U8HVqndOi6UfDCAABpB6VN6mfuUCwtro6oPMGb0HvVxpTvDby57565bStDtoCOXXu+y61Pyvgy8xujiUGGh3oKeduY9FtdR09vV9XXF7n/23SPi9QdFtdbFudo9cuL6tF04/GIwAEkPgqSmuCuJrArbjmvYpXi+oYom0U4VKumxvMuTl0gc967RT8fSY5G0h8VVWOrYtnG2EE6udtMPOXbzAbSivC/o9KZjft2KomyKvOzVPu3mZdWpv8HMKHZMcWBNDyfdDZAG71xkDOBnc17wPj3GF19WPDGkN90rVyAzkFbp1CPocEeQr+6HAYSaxcfeitKrI5eW6Qpxw9tcItragK+z+Z6Wmmb+f8QG6eW0evf5d8k5uV0eLLgJbBmQ5A0/qeCwrm1mz8HAjm3O9qhvLCxv1eWoYxrTrWBHGdqoM1NxfOfu5Y89kzjVrFUsEcKdqHnvrLs0FePX3oeWVnppv+CvQ8dfP+0q216dMp32RlUNfUbwgAAWzMlbONIGpeA8Gb+947rmZ8Vfiio4iDOQVtgYCt5tWO6xQyrgN90cGXisoqzK8rCgP957nFtr+tKjR1dKFnWmVn2GJaG+R129jitneHPJNJoIcaBIBASjV8KK4J2Ooa3IBubXCwV1Hc+N/NzKupN9dx42sgkHODvE6e7ztVN5wgmAMCCmr60PvF09pWwZ7q7dVFfehtzMlrYzaraZRBH3qIBAEgkGiqKo0pWbcxUCvxBGz2s/veO75mqCxt/O8GcuU61AxuAKf37UMCOs9rVl4slx5IaasLy8y8ZRsbYrhFt8sK6j52O+VnBzpIVvcqapChotsubehDD41HAAg0ZxCnYM0Gc27g5nlVwFZr3FpjStc17bf13FY3iFOxaVBQFzLoO3ca5cpxMQFi0rWKAjpvJ8lu0e2qwrr70OveNndjIwxP0W3HfLoOQuwRAAL1dT1igzhPAOfmzIUGdkFB3jpjSgsa16eclxowuEFcnju4nz25coGAruZzdj6BHNBCXav8uba41jNuVYy7vo6uVaR3x7yanLyNnSWr+LZtLn3ooeUQACJ1VZRVB2LenLi6Bhu8ecetbXw/cuHqxyk4cwO5sK8dggM8PS2CDoOBBOtapbqj5ECO3ooNpqQ8fNcqGelppk/HVrVy89S1SqtsLr2IP/ZCJG6DBvX9VlITwAUCuYLqIlL3feh33iCuKQ0bAtKMyW1bHZC5gZkbtAW9bx9mvII4OgkGkkVJeaVtcVudi7c+8Ag0tbits2uVjHQb1LkNMNxAr2/nViYnkz70kLgIABF7VVXVwZsNzAqCX8ONC3qtCd7Uv5wT/s46aqrbpmDMfbUBnRuw1Yx33wcN7Y3JaWNMOidxIJWsLyk389W1Sk2QN78mR2/R6qKIulYJBHvd2tC1CpIWASBqdyOi4MsONYGYBhuc6b03WHM/h36/vun137wNGmzg5gZvbiDXPsw4N7jzBHG2uxECOMCPVm0oDWptO78mR29pQd3VO9qqaxVPR8l0rYJURQCYCirLq4Mum+u2YWPQVuYGchpqcuQCn0OHmu+cytilKz2rOiBzA7ickPe1Xr3BXM04dTFCgwYA9TTEWLyuOCjIc9+vKSqv8/+6tsnxPPasOtDT+y6t6VoF/kAAmIgWfWnMos9rAjo3sKsJ4tyi1cD79bFprOCVlm5MdpuaIKxNzaD3rWte9V276laq9QV4qv/GiRRADJRVqCFGYXCQZ4tvC01xefgbV51+enXIC9TLU8tbN9Brl0eLW/gbAWAi+mWGMe/fEv3/ZeZWB2WBoK2NJ0ireR8ayAUFeO7/0I0IgPjVz7MNMQIBXvXrwlVFpqKOCnpZGWmmb6f8QI6eO/Tv3NrkZVMFBAiHADARdR9ozMBjagK21tW5cfbVDe5qgrjQYC+DO1oAydFR8vL1pYHcvPme3Lz66uflqyFGSE6eBnW3QkMMIDoEgIloq2HVAwAksdKKStt/3vyaQE85ezbgW1FoNtTTUbIecbZZl+ocPbW8dQM9PSmD+nlAbBAAAgCalJu3YkOpDe6qh5pgb2Vhvd2qqKPkTTu2CgR4CviUq7dZ59amXStKM4DmRgAIAGhQcVml7RBZQd6ClTW5eSurA771JXXn5rXJyTT93QBPRbc24Ms3m3bMN9mZFNsC8UIACACwKiqrzB9ris2ClYW1Bj3zti4qld2kfV4gwNOTMTTQrQqQuAgAAcBHKtVv3trqIE/dqixYWWRz9H5bVWSLbOtqaSvqOkWBXb/O1bl5/Tsr0Gtt+nRqZXKzaG0LJBMCQABIwT7z/lhTZH5fXWS7T1HR7e81rwry6nqureRkptsAL3RQoNcxP7tFlwNA8yEABIAkbHihp1womFu4usgsWlMd6Om9Ar0l64rrbHwh2RnpZtNOrWzfeX07tTL9lKun953zbUtbHnkGpD4CQABIwACvoLjC/LG2yPy5ptjWy1OQt2i13hfZz/V1oyJ5WRm2aLZ6yK9+7aggr5Xp0S7PtsIF4F8EgADQwsorq8zSdSW202PVx1MDC70uXltiAz59bijAk25tc2xXKr01dHCDverPNL4AUB8CQACIYc6dAjc95WJZQYkdlq4rNUvXFdtgb2lBqVmyttj2m+fUU0Tr6pSfbTbpkGefZ6sAT6+9aoI9vafhBYDGIgAEgAaUlFeaVYVlZuX6UrOqsNSsXF9mg7gV60urXwuqXxXwFZVVRjRP1cPr3i7X9GiXa7tQUaDXs32e/dyr5n2rbE7RAJqH788u9957r7ntttvM0qVLzfbbb2/uueces+uuu8Y7WQCaKYeuuLzSrCsuN2uLymtey2yDijV6Lax+v7qwzAZ8qwtLzeoNZaYwwqDO2/lx17Y5plvbXBvkda951eee7fJMj/a5pmOrbBpbAIgbXweAzz77rLn44ovNpEmTzG677WbuuusuM3jwYDN37lzTtWvXeCcPQEjgVlhaaZ9IUVhWYQpLK2xxq8bp/Xp9Lqkw60vK7Xg9naKgpNwUaFyxXsttw4qyyqpGpSErI810ys8xndtk21c9r1ZD15rXzq1zbKCnwI+cOwCJLs3RmdWnFPTtsssu5j//+Y/9XFVVZXr37m3OP/98M2bMmAb/v6CgwLRr186sW7fOtG3btgVSDDQvnQ7UfUhFVZWpqqp+VcfB6hxYr2q8UFFZ/VnflVc4pty+VtlxCq70Xv3MlVVW2u9LK6tMaXml/U7902kotUOlKS2vfq8iVgV4+lxSUR3kqShV4/Wq72IpMz3NtG+VZdrmZZn2eVmmQ6ts0yE/23RolWXat1KAV/1Zr+r7TgFf27xMGlUAKaKA67d/cwDLysrM119/bcaOHRsYl56ebg466CDz6aefhv2f0tJSO3h3oOYw/fslZvr3S01Laek7gMbcckTyL+HuZcL+X8hIJ8xUobPyfnan1zgn6Hun1vR6cdPl1DXezsepfq3rvSc4c99rfJU7Luhz8HcK3Nz/rdSrgjmnOqDTdHqt9IxLdK2yM+yQn5Np8rMzTWu95lR/bpObZdrkZtoi2NZ6zc2yT69om5tpgz034NP/E8wB8DPfBoArV640lZWVplu3bkHj9XnOnDlh/+emm24y1157bbOnbc7S9ealWYub/XeAaHPN1HdcVkZ6zWv1+0y9ple/Zmem23Ea1MhB0+RkZtjx7qAnTWicfc2qfp+blW5y7Wv1+7zsDNuPnYpS9arPCvI0DfXmAKDpfBsANoZyC1Vn0JsDqCLjWNt78y42VyMZNGcuSiRzDv35tEam2/0YNDZ0mrDTp4X9f312v9PLxvFpwdPUTBf4/5rv9Tnd815TKe7R9+me/1MwpK/ttDX/o+81vjpOqg7aMjzf28/p7rTu5+rB/axgLrPmsxv4kWMGAKkjOaKMZtC5c2eTkZFhli1bFjRen7t37x72f3JycuzQ3Hbq08EOAAAAzSHd+FR2drbZaaedzNtvvx0Yp0Yg+rzHHnvENW0AAADNybc5gKLi3JEjR5qdd97Z9v2nbmAKCwvNaaedFu+kAQAANBtfB4DHHXecWbFihRk3bpztCHqHHXYw06dPr9UwBAAAIJX4uh/ApqIfIQAAkk8B12//1gEEAADwKwJAAAAAnyEABAAA8BkCQAAAAJ8hAAQAAPAZAkAAAACfIQAEAADwGQJAAAAAnyEABAAA8BlfPwquqdyHqKhHcQAAkBwKaq7bfn4YGgFgE6xfv96+9u7dO95JAQAAjbiOt2vXzvgRzwJugqqqKrN48WLTpk0bk5aWFvO7EwWWixYtSsnnFLJ8yS/Vl5HlS36pvowsX+M5jmODv549e5r0dH/WhiMHsAm00/Tq1atZf0M7fSoe2C6WL/ml+jKyfMkv1ZeR5Wucdj7N+XP5M+wFAADwMQJAAAAAnyEATFA5OTlm/Pjx9jUVsXzJL9WXkeVLfqm+jCwfmoJGIAAAAD5DDiAAAIDPEAACAAD4DAEgAACAzxAAAgAA+AwBYJxMmDDB7LnnnqZVq1amffv2Ef2P2uuMGzfO9OjRw+Tl5ZmDDjrIzJs3L2ia1atXm5NOOsl2mqn5nnHGGWbDhg2mpUWbjt9++80+TSXc8NxzzwWmC/f9M888Y+KhMet6v/32q5X+s88+O2iahQsXmsMOO8zuG127djWXXXaZqaioMIm+fJr+/PPPN1tssYXdPzfddFMzevRos27duqDp4rkN7733XtO3b1+Tm5trdtttN/PFF1/UO732vS233NJOP3DgQPO///0v6mOyJUWzfA899JDZe++9TYcOHeygtIdOf+qpp9baVkOGDDHJsHyPP/54rbTr/xJ5+0W7jOHOJxp0/kjEbfjBBx+YYcOG2advKB0vvfRSg//z3nvvmR133NG2BB4wYIDdrk09rlFDrYDR8saNG+fccccdzsUXX+y0a9cuov+5+eab7bQvvfSSM3v2bGf48OFOv379nOLi4sA0Q4YMcbbffnvns88+cz788ENnwIABzgknnOC0tGjTUVFR4SxZsiRouPbaa53WrVs769evD0ynXfaxxx4Lms67/C2pMet63333dc4888yg9K9bty5oPWy77bbOQQcd5HzzzTfO//73P6dz587O2LFjnURfvu+++8456qijnFdeecX55ZdfnLffftvZfPPNnaOPPjpounhtw2eeecbJzs52Hn30UeeHH36w26F9+/bOsmXLwk7/8ccfOxkZGc6tt97q/Pjjj85VV13lZGVl2eWM5phsKdEu34knnujce++9dj/76aefnFNPPdUuyx9//BGYZuTIkXY/8G6r1atXO/EQ7fJpH2vbtm1Q2pcuXRo0TSJtv8Ys46pVq4KW7/vvv7f7rJY9Ebehzmf/+te/nBdffNGeB6ZNm1bv9L/++qvTqlUre53UMXjPPffY5Zs+fXqj1xk2IgCMMx2okQSAVVVVTvfu3Z3bbrstMG7t2rVOTk6OM2XKFPtZB4gOqi+//DIwzeuvv+6kpaU5f/75p9NSYpWOHXbYwTn99NODxkVy0kjkZVQAeMEFF9R7gkxPTw+6UN1///32QlZaWuok2zacOnWqPTmXl5fHfRvuuuuuzqhRowKfKysrnZ49ezo33XRT2OmPPfZY57DDDgsat9tuuzn//Oc/Iz4mE3n5Qunmo02bNs4TTzwRFDyMGDHCSQTRLl9D59ZE236x2IZ33nmn3YYbNmxIyG3oFcl54PLLL3e22WaboHHHHXecM3jw4JitMz+jCDhJLFiwwCxdutQWUXifY6js7k8//dR+1quK6nbeeefANJpezyz+/PPPWyytsUjH119/bWbNmmWLHUONGjXKdO7c2ey6667m0UcftcU4La0pyzh58mSb/m233daMHTvWFBUVBc1XRY3dunULjBs8eLB9KPoPP/xgWkqs9iUV/6oIOTMzM67bsKyszO5T3uNHy6LP7vETSuO907vbwp0+kmOypTRm+UJpPywvLzcdO3asVQSnqggq2j/nnHPMqlWrTEtr7PKpykKfPn1M7969zYgRI4KOoUTafrHaho888og5/vjjTX5+fsJtw8Zo6BiMxTrzs+CzMhKWTlTiDQzcz+53etVB7qULr07o7jQtldampkMnsq222srWk/S67rrrzAEHHGDrx7355pvm3HPPtSd51TVrSY1dxhNPPNFekFQH5ttvvzVXXHGFmTt3rnnxxRcD8w23jd3vkmkbrly50lx//fXmrLPOivs2VFoqKyvDrts5c+aE/Z+6toX3eHPH1TVNS2nM8oXSvqj90nsxVV2xo446yvTr18/Mnz/fXHnllebQQw+1F9eMjAyTyMunYEc3F9ttt529EZk4caI9nygI7NWrV0Jtv1hsQ9V7+/777+250ytRtmFj1HUM6oa4uLjYrFmzpsn7vZ8RAMbQmDFjzC233FLvND/99JOtVJ7Ky9dUOrCffvppc/XVV9f6zjtu0KBBprCw0Nx2220xCx6aexm9wZBy+lT5/MADD7Qn5s0228ykyjbUCVoV0bfeemtzzTXXtOg2RPRuvvlm2xBHOUXehhLKTfLurwqmtJ9qOu23iWyPPfawg0vBn24qH3jgAXtjkmoU+GkbKVfdK5m3IZoXAWAMXXLJJbbFVX369+/fqHl3797dvi5btswGDS593mGHHQLTLF++POj/1HpUrTPd/2+J5WtqOp5//nlbHHXKKac0OK2Ka3QyLy0tjcnzIltqGb3pl19++cWelPW/oS3YtI0lWbbh+vXrba5DmzZtzLRp00xWVlaLbsNwVNys3A53Xbr0ua7l0fj6po/kmGwpjVk+l3LGFAC+9dZbNjhoaN/Qb2l/bcngoSnL59J+qBsOpT3Rtl9Tl1E3UQrglbvekHhtw8ao6xhUtRK12tb6aup+4WvxroTod9E2Apk4cWJgnFqPhmsE8tVXXwWmeeONN+LWCKSx6VBDidCWo3W54YYbnA4dOjgtLVbr+qOPPrLzUQtEbyMQbwu2Bx54wDYCKSkpcRJ9+bRP7r777nYbFhYWJtQ2VGXx8847L6iy+CabbFJvI5DDDz88aNwee+xRqxFIfcdkS4p2+eSWW26x+9ann34a0W8sWrTI7gMvv/yykwzLF9rIZYsttnAuuuiihNx+TVlGXUeU7pUrVyb0NmxMIxD1iuClnghCG4E0Zb/wMwLAOPn9999t9wtuVyd6r8Hb5YlOVmou7+2yQM3bdeB+++23tmVXuG5gBg0a5Hz++ec2uFA3HPHqBqa+dKirCS2fvveaN2+ePTmpxWkodS/y0EMP2W44NN19991nuwhQlzrxEO0yqmuU6667zgZVCxYssNuxf//+zj777FOrG5hDDjnEmTVrlu3uoEuXLnHrBiaa5dPFU61kBw4caJfV2+2Elive21DdRegi+fjjj9sA96yzzrLHk9vi+u9//7szZsyYoG5gMjMzbYCgblLGjx8fthuYho7JlhLt8intaqH9/PPPB20r9xyk10svvdQGh9pf33rrLWfHHXe0+0FL3ow0dvl0btVNy/z5852vv/7aOf74453c3FzbVUgibr/GLKNrr732sq1jQyXaNlR63GudAkB1hab3uh6Klk3LGNoNzGWXXWaPQXVbFK4bmPrWGepGABgnapqvAyB0ePfdd2v1l+bSHevVV1/tdOvWze7wBx54oDN37txa/ULpIq2gUnf2p512WlBQ2VIaSodORqHLKwp0evfube/iQikoVNcwmmd+fr7to27SpElhp03EZVy4cKEN9jp27Gi3n/rV04nN2w+g/Pbbb86hhx7q5OXl2T4AL7nkkqBuVBJ1+fQabp/WoGkTYRuqH7FNN93UBj7KOVAfhy7lWuq4DO3G5i9/+YudXt1RvPbaa0HfR3JMtqRolq9Pnz5ht5UCXSkqKrI3IroBUeCr6dXHWjwvrNEs34UXXhiYVttn6NChzsyZMxN6+zVmH50zZ47dbm+++WateSXaNqzrHOEuk161jKH/o3OG1odumL3XxEjWGeqWpj/xLoYGAABAy6EfQAAAAJ8hAAQAAPAZAkAAAACfIQAEAADwGQJAAAAAnyEABAAA8BkCQAAAAJ8hAASAGFq6dKk5+OCDTX5+vmnfvn2z/Mbf//53c+ONN5pEMH36dPvs3KqqqngnBUAUCAABHzn11FNNWlparWHIkCEmWe23337mwgsvNInizjvvNEuWLDGzZs0yP//8c8znP3v2bPO///3PjB492jSngQMHmrPPPjvsd//9739NTk6OWblypd13srKyzOTJk5s1PQBiiwAQ8BldsBWgeIcpU6Y062+WlZWZeNIDjyoqKlrkt+bPn2922mkns/nmm5uuXbvGfH3dc8895phjjjGtW7c2zemMM84wzzzzjCkuLq713WOPPWaGDx9uOnfuHLix+Pe//92s6QEQWwSAgM8o56Z79+5BQ4cOHQLfK0fw4YcfNkceeaRp1aqVDWReeeWVoHl8//335tBDD7VBSLdu3WyRpHKDvLly5513ns2ZU5AwePBgO17z0fxyc3PN/vvvb5544gn7e2vXrjWFhYWmbdu25vnnnw/6rZdeeskWp65fv77WsijweP/9983dd98dyM387bffzHvvvWffv/766zYY0zJ/9NFHNjgbMWKETbPSvssuu5i33noraJ59+/a1xaunn366adOmjdl0003Ngw8+GBScadl69Ohhl6NPnz7mpptuCvzvCy+8YJ588kn7+0qfaPn+8Y9/mC5duthlPOCAA2xOnuuaa66xxaha7/369bPzDaeystKun2HDhtVK8w033GBOOeUUu1xKk9b1ihUr7PJq3HbbbWe++uqroP/TOtl7771NXl6e6d27t81V1HaQk08+2QZ/Wh6vBQsW2PWrANGl9GjeWr8AkkQ9zwkGkGL0sPURI0bUO41OC7169XKefvppZ968ec7o0aOd1q1bO6tWrbLfr1mzxj5cfuzYsc5PP/3kzJw50zn44IOd/fffPzAPPdBd/3PZZZfZh9Vr+PXXX+0D6S+99FL7ecqUKc4mm2xif0/zFD2ofujQoUHpGT58uHPKKaeETevatWudPfbYw/7fkiVL7FBRURF46Px2223nvPnmm84vv/xi0z9r1ixn0qRJznfffef8/PPPzlVXXeXk5uY6v//+e2Ceffr0cTp27Ojce++9dvlvuukmJz093aZZbrvtNqd3797OBx984Pz222/Ohx9+aNeVLF++3BkyZIhz7LHH2rQofXLQQQc5w4YNc7788kv7u5dcconTqVOnwDodP368k5+fb/9X63P27Nlhl1ffabmWLl0aNN5Ns5ZN8z/nnHOctm3b2vlNnTrVmTt3rnPEEUc4W221lVNVVWX/R+tEv3nnnXfa//n444+dQYMGOaeeempgvsccc0zQdpVx48bZ5a+srAwa361bN+exxx6rY68CkGgIAAGfBYAZGRn2wu8dJkyYEJhGAYYCI9eGDRvsuNdff91+vv76651DDjkkaL6LFi2y0yjQcANABRNeV1xxhbPtttsGjfvXv/4VFAB+/vnnNn2LFy+2n5ctW+ZkZmY67733Xp3LpN+64IILgsa5AeBLL73U4DrZZpttnHvuuScomDr55JMDnxUwde3a1bn//vvt5/PPP9854IADAoFUKAXYWs8uBYgKxkpKSoKm22yzzZwHHnggEAAqOFYAWZ9p06bZ9RP626FpVvCp5b/66qsD4z799FM7Tt/JGWec4Zx11llB81FaFewWFxfbz9OnT3fS0tJs8O6uC/2Wd/9waXtfc8019aYfQOKgCBjwGRW9qoGCdwit7K/iQpeKX1VsuXz5cvtZRZfvvvuuLVZ0hy233NJ+5y0CVNGr19y5c22Rq9euu+5a6/M222xji4blqaeessWZ++yzT6OWdeeddw76vGHDBnPppZearbbayrbQVdp/+ukns3DhwjqXX0W5KiZ3l1/FulpnW2yxhS0yffPNN+tNg9aXfrdTp05B60xFqd71peVUEXF9VCSr4mylKZQ3zSridhtyhI7zbsfHH388KE0qqldrXqVN1Jq5V69ets6fvP3223ZdnXbaabV+X8XIRUVF9aYfQOLIjHcCALQsBXQDBgyodxq16vRSwOF286FgRnW+brnlllr/p3px3t9pDNWVu/fee82YMWNs4KFgI1zAE4nQNCj4mzFjhpk4caJdBwpa/va3v9VqdFHf8u+44442QFL9QtUfPPbYY81BBx1Uq+6iS+tL60X15kJ5u4mJZH2pPqWCLKU3Ozu7zjS76yvcOO92/Oc//xm2NbHqPUp6eroNeBWQq56itoduIPr371/rf1avXt1gAAsgcRAAAoiKAiA1DFDDg8zMyE8hyjFT9yVeX375Za3p1Pjg8ssvt61Kf/zxRzNy5Mh656tASI0jIvHxxx/bgEYNXNwgSI1GoqUc0eOOO84OCiDVsloBUMeOHcOuL/UNqHWlddYUaigiWi/u+8ZSujSfhm4GFICrgcmLL75opk2bZhuqhCopKbG5mYMGDWpSmgC0HIqAAZ8pLS21AYl38LbgbcioUaNssHPCCSfYAE4X/jfeeMMGCvUFYsptmjNnjrniiits/3hTp061RZDizeFTi+SjjjrKXHbZZeaQQw6xRZD1UVD1+eef20BOy1Ffh8RqgaxARkW4KgI98cQTo+7A+I477rDd5mhZtBzPPfecLSKuq9Nn5Q7uscce5ogjjrDFxUrnJ598Yv71r3/VapXbEOWwKXBT692m0nZQOtSiWetj3rx55uWXX7afvdQqWa2WzzrrLFv8rG0T6rPPPrPfaTkBJAcCQMBn9OQGFUl6h7322ivi/+/Zs6fNSVOwpwBN9czU3YsCIBUZ1kWBhIpJFYCpvtr9999vgyBR8OClLkZUzKmuWBqiYt2MjAyz9dZb2wAptD5faPCmAHPPPfe0xdiq86aAKhrqGubWW2+19QtVp1EBnXI261p2Bbf6XvUYFST/5S9/Mccff7z5/fffA/Xyoi0ij0Wny9oG6kJHQay6glHu3bhx4+z2DaXtsWbNGhswh+uiRgHxSSedZLsNApAc0tQSJN6JAOBPEyZMMJMmTTKLFi2q9aSJiy66yCxevLhWXTe/U0MQFac/++yzCZHjplxXpUe5mQryASQH6gACaDH33XefzTVTi1jlIt52221BRY5q4KAnk9x88822yJjgrzY1XFFH09EU2zcn5YBquxL8AcmFHEAALUa5esq5Uh1CtTTVE0TGjh0baEyilqbKFVRxqeqjNffjzgDArwgAAQAAfIZGIAAAAD5DAAgAAOAzBIAAAAA+QwAIAADgMwSAAAAAPkMACAAA4DMEgAAAAD5DAAgAAOAzBIAAAADGX/4fCJ0j4Wf4CTIAAAAASUVORK5CYII=",
- "text/html": [
- "\n",
- " \n",
- "
\n",
- " Figure\n",
- "
\n",
- "

\n",
- "
\n",
- " "
- ],
- "text/plain": [
- "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …"
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
+ "outputs": [],
"source": [
"import scipp as sc\n",
"temperatures=[1, 10, 100]\n",
diff --git a/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py
index a64ffd2..875020f 100644
--- a/src/easydynamics/sample_model/__init__.py
+++ b/src/easydynamics/sample_model/__init__.py
@@ -1,3 +1,4 @@
+from .component_collection import ComponentCollection
from .components import (
DampedHarmonicOscillator,
DeltaFunction,
@@ -8,7 +9,7 @@
)
__all__ = [
- "SampleModel",
+ "ComponentCollection",
"Gaussian",
"Lorentzian",
"Voigt",
diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py
new file mode 100644
index 0000000..6b31dce
--- /dev/null
+++ b/src/easydynamics/sample_model/component_collection.py
@@ -0,0 +1,303 @@
+import warnings
+from typing import List
+
+import numpy as np
+import scipp as sc
+
+# from easyscience.job.theoreticalmodel import TheoreticalModelBase
+from easyscience.base_classes.model_base import ModelBase
+from easyscience.variable import DescriptorBase, Parameter
+
+from .components.model_component import ModelComponent
+
+Numeric = float | int
+
+
+class ComponentCollection(ModelBase):
+ """
+ A model of the scattering from a sample, combining multiple model components.
+
+ Attributes
+ ----------
+ display_name : str
+ Display name of the ComponentCollection.
+ unit : str or sc.Unit
+ Unit of the ComponentCollection.
+
+ """
+
+ def __init__(
+ self,
+ unit: str | sc.Unit = "meV",
+ display_name: str = "MyComponentCollection",
+ unique_name: str | None = None,
+ components: List[ModelComponent] | None = None,
+ ):
+ """
+ Initialize a new ComponentCollection.
+
+ Parameters
+ ----------
+ unit : str or sc.Unit, optional
+ Unit of the sample model. Defaults to "meV".
+ display_name : str
+ Display name of the sample model.
+ unique_name : str or None, optional
+ Unique name of the sample model. Defaults to None.
+ components : List[ModelComponent], optional
+ Initial model components to add to the ComponentCollection.
+ """
+
+ super().__init__(display_name=display_name)
+
+ if unit is not None and not isinstance(unit, (str, sc.Unit)):
+ raise TypeError(
+ f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}"
+ )
+ self._unit = unit
+ self._components = []
+
+ # Add initial components if provided. Used for serialization.
+ if components is not None:
+ if not isinstance(components, list):
+ raise TypeError(
+ "components must be a list of ModelComponent instances."
+ )
+ for comp in components:
+ self.add_component(comp)
+
+ def add_component(self, component: ModelComponent) -> None:
+ if not isinstance(component, ModelComponent):
+ raise TypeError("Component must be an instance of ModelComponent.")
+
+ if component in self._components:
+ raise ValueError(
+ f"Component '{component.unique_name}' is already in the collection."
+ )
+
+ self._components.append(component)
+
+ def remove_component(self, unique_name: str) -> None:
+ if not isinstance(unique_name, str):
+ raise TypeError("Component name must be a string.")
+
+ for comp in self._components:
+ if comp.unique_name == unique_name:
+ self._components.remove(comp)
+ return
+
+ raise KeyError(f"No component named '{unique_name}' exists.")
+
+ @property
+ def components(self) -> list[ModelComponent]:
+ return list(self._components)
+
+ def list_component_names(self) -> List[str]:
+ """
+ List the names of all components in the model.
+
+ Returns
+ -------
+ List[str]
+ Component names.
+ """
+
+ return [component.unique_name for component in self._components]
+
+ def clear_components(self) -> None:
+ """Remove all components."""
+ self._components.clear()
+
+ def normalize_area(self) -> None:
+ # Useful for convolutions.
+ """
+ Normalize the areas of all components so they sum to 1.
+ """
+ if not self.components:
+ raise ValueError("No components in the model to normalize.")
+
+ area_params = []
+ total_area = Parameter(name="total_area", value=0.0, unit=self._unit)
+
+ for component in self.components:
+ if hasattr(component, "area"):
+ area_params.append(component.area)
+ total_area += component.area
+ else:
+ warnings.warn(
+ f"Component '{component.unique_name}' does not have an 'area' attribute and will be skipped in normalization.",
+ UserWarning,
+ )
+
+ if total_area.value == 0:
+ raise ValueError("Total area is zero; cannot normalize.")
+
+ if not np.isfinite(total_area.value):
+ raise ValueError("Total area is not finite; cannot normalize.")
+
+ for param in area_params:
+ param.value /= total_area.value
+
+ def get_all_variables(self) -> list[DescriptorBase]:
+ """
+ Get all parameters from the model component.
+ Returns:
+ List[Parameter]: List of parameters in the component.
+ """
+
+ return [
+ var
+ for component in self.components
+ for var in component.get_all_variables()
+ ]
+
+ @property
+ def unit(self) -> str | sc.Unit:
+ """
+ Get the unit of the ComponentCollection.
+
+ Returns
+ -------
+ str or sc.Unit or None
+ """
+ return self._unit
+
+ @unit.setter
+ def unit(self, unit_str: str) -> None:
+ raise AttributeError(
+ (
+ f"Unit is read-only. Use convert_unit to change the unit between allowed types "
+ f"or create a new {self.__class__.__name__} with the desired unit."
+ )
+ ) # noqa: E501
+
+ def convert_unit(self, unit: str | sc.Unit) -> None:
+ """
+ Convert the unit of the ComponentCollection and all its components.
+ """
+
+ old_unit = self._unit
+
+ try:
+ for component in self.components:
+ component.convert_unit(unit)
+ self._unit = unit
+ except Exception as e:
+ # Attempt to rollback on failure
+ try:
+ for component in self.components:
+ component.convert_unit(old_unit)
+ except Exception:
+ pass # Best effort rollback
+ raise e
+
+ def evaluate(
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
+ ) -> np.ndarray:
+ """
+ Evaluate the sum of all components.
+
+ Parameters
+ ----------
+ x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
+ Energy axis.
+
+ Returns
+ -------
+ np.ndarray
+ Evaluated model values.
+ """
+
+ if not self.components:
+ raise ValueError("No components in the model to evaluate.")
+ return sum(component.evaluate(x) for component in self.components)
+
+ def evaluate_component(
+ self,
+ x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
+ unique_name: str,
+ ) -> np.ndarray:
+ """
+ Evaluate a single component by name.
+
+ Parameters
+ ----------
+ x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
+ Energy axis.
+ unique_name : str
+ Component unique name.
+
+ Returns
+ -------
+ np.ndarray
+ Evaluated values for the specified component.
+ """
+ if not self.components:
+ raise ValueError("No components in the model to evaluate.")
+
+ if not isinstance(unique_name, str):
+ raise TypeError(
+ (
+ f"Component unique name must be a string, got {type(unique_name)} instead."
+ )
+ )
+
+ matches = [comp for comp in self.components if comp.unique_name == unique_name]
+ if not matches:
+ raise KeyError(f"No component named '{unique_name}' exists.")
+
+ component = matches[0]
+
+ result = component.evaluate(x)
+
+ return result
+
+ def fix_all_parameters(self) -> None:
+ """
+ Fix all free parameters in the model.
+ """
+ for param in self.get_fittable_parameters():
+ param.fixed = True
+
+ def free_all_parameters(self) -> None:
+ """
+ Free all fixed parameters in the model.
+ """
+ for param in self.get_fittable_parameters():
+ param.fixed = False
+
+ def __contains__(self, item: str | ModelComponent) -> bool:
+ """
+ Check if a component with the given name or instance exists in the ComponentCollection.
+ Args:
+ ----------
+ item : str or ModelComponent
+ The component name or instance to check for.
+ Returns
+ -------
+ bool
+ True if the component exists, False otherwise.
+ """
+
+ if isinstance(item, str):
+ # Check by component unique name
+ return any(comp.unique_name == item for comp in self.components)
+ elif isinstance(item, ModelComponent):
+ # Check by component instance
+ return any(comp is item for comp in self.components)
+ else:
+ return False
+
+ def __repr__(self) -> str:
+ """
+ Return a string representation of the ComponentCollection.
+
+ Returns
+ -------
+ str
+ """
+ comp_names = (
+ ", ".join(c.unique_name for c in self.components) or "No components"
+ )
+
+ return f""
diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
index d1fd72d..69a0681 100644
--- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
+++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import Optional, Union
-
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
@@ -10,7 +8,7 @@
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
@@ -18,7 +16,7 @@ class DampedHarmonicOscillator(CreateParametersMixin, 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.
+ display_name (str): Display 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.
@@ -27,33 +25,77 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
def __init__(
self,
- name: Optional[str] = "DampedHarmonicOscillator",
- area: Optional[Union[Numeric, Parameter]] = 1.0,
- center: Optional[Union[Numeric, Parameter]] = 1.0,
- width: Optional[Union[Numeric, Parameter]] = 1.0,
- unit: Optional[Union[str, sc.Unit]] = "meV",
+ area: Numeric | Parameter = 1.0,
+ center: Numeric | Parameter = 1.0,
+ width: Numeric | Parameter = 1.0,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "DampedHarmonicOscillator",
+ unique_name: str | None = None,
):
- # Validate inputs and create Parameters if not given
- self.validate_unit(unit)
- self._unit = unit
+ super().__init__(
+ display_name=display_name,
+ unique_name=unique_name,
+ unit=unit,
+ )
# These methods live in ValidationMixin
- area = self._create_area_parameter(area=area, name=name, unit=self._unit)
+ area = self._create_area_parameter(
+ area=area, name=display_name, unit=self._unit
+ )
center = self._create_center_parameter(
- center=center, name=name, fix_if_none=False, unit=self._unit
+ center=center,
+ name=display_name,
+ fix_if_none=False,
+ unit=self._unit,
+ enforce_minimum_center=True,
)
- width = self._create_width_parameter(width=width, name=name, unit=self._unit)
- super().__init__(
- name=name,
- unit=unit,
- area=area,
- center=center,
- width=width,
+ width = self._create_width_parameter(
+ width=width, name=display_name, unit=self._unit
)
+ self._area = area
+ self._center = center
+ self._width = width
+
+ @property
+ def area(self) -> Parameter:
+ """Get the area parameter."""
+ return self._area
+
+ @area.setter
+ def area(self, value: Numeric) -> None:
+ """Set the area parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("area must be a number")
+ self._area.value = value
+
+ @property
+ def center(self) -> Parameter:
+ """Get the center parameter."""
+ return self._center
+
+ @center.setter
+ def center(self, value: Numeric) -> None:
+ """Set the center parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("center must be a number")
+ self._center.value = value
+
+ @property
+ def width(self) -> Parameter:
+ """Get the width parameter."""
+ return self._width
+
+ @width.setter
+ def width(self, value: Numeric) -> None:
+ """Set the width parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("width must be a number")
+ self._width.value = value
+
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: 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
@@ -62,13 +104,12 @@ def evaluate(
x = self._prepare_x_for_evaluate(x)
normalization = 2 * self.center.value**2 * self.width.value / np.pi
+ # No division by zero here, width>0 enforced in setter
denominator = (x**2 - self.center.value**2) ** 2 + (
- 2
- * self.width.value
- * x # No division by zero here, width>0 enforced in setter
+ 2 * self.width.value * x
) ** 2
return self.area.value * normalization / (denominator)
def __repr__(self):
- return f"DampedHarmonicOscillator(name = {self.name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
+ return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py
index bb9317a..2480c71 100644
--- a/src/easydynamics/sample_model/components/delta_function.py
+++ b/src/easydynamics/sample_model/components/delta_function.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import Optional, Union
-
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
@@ -10,7 +8,7 @@
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
EPSILON = 1e-8 # small number to avoid floating point issues
@@ -21,38 +19,68 @@ class DeltaFunction(CreateParametersMixin, ModelComponent):
If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.
Args:
- name (str): Name of the component.
center (Int or float or None): Center of the delta function. If None, defaults to 0 and is fixed.
area (Int or float): Total area under the curve.
unit (str or sc.Unit): Unit of the parameters. Defaults to "meV".
+ display_name (str): Name of the component.
+ unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated.
"""
def __init__(
self,
- name: Optional[str] = "DeltaFunction",
- center: Optional[Union[None, Numeric, Parameter]] = None,
- area: Optional[Union[Numeric, Parameter]] = 1.0,
- unit: Union[str, sc.Unit] = "meV",
+ center: None | Numeric | Parameter = None,
+ area: Numeric | Parameter = 1.0,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "DeltaFunction",
+ unique_name: str | None = None,
):
# Validate inputs and create Parameters if not given
- self.validate_unit(unit)
- self._unit = unit
+ super().__init__(
+ display_name=display_name,
+ unit=unit,
+ unique_name=unique_name,
+ )
# These methods live in ValidationMixin
- area = self._create_area_parameter(area=area, name=name, unit=self._unit)
+ area = self._create_area_parameter(
+ area=area, name=display_name, unit=self._unit
+ )
center = self._create_center_parameter(
- center=center, name=name, fix_if_none=True, unit=self._unit
+ center=center, name=display_name, fix_if_none=True, unit=self._unit
)
- super().__init__(
- name=name,
- unit=unit,
- area=area,
- center=center,
- )
+ self._area = area
+ self._center = center
+
+ @property
+ def area(self) -> Parameter:
+ """Get the area parameter."""
+ return self._area
+
+ @area.setter
+ def area(self, value: Numeric) -> None:
+ """Set the area parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("area must be a number")
+ self._area.value = value
+
+ @property
+ def center(self) -> Parameter:
+ """Get the center parameter."""
+ return self._center
+
+ @center.setter
+ def center(self, value: Numeric | None) -> None:
+ """Set the center parameter value."""
+ if value is None:
+ value = 0.0
+ self._center.fixed = True
+ if not isinstance(value, Numeric):
+ raise TypeError("center must be a number")
+ self._center.value = value
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
"""Evaluate the Delta function at the given x values.
The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area.
@@ -88,4 +116,4 @@ def evaluate(
return model
def __repr__(self):
- return f"DeltaFunction(name = {self.name}, unit = {self._unit},\n area = {self.area},\n center = {self.center}"
+ return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n area = {self.area},\n center = {self.center}"
diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py
index 239f664..d642c5a 100644
--- a/src/easydynamics/sample_model/components/gaussian.py
+++ b/src/easydynamics/sample_model/components/gaussian.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import Optional, Union
-
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
@@ -10,7 +8,7 @@
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
class Gaussian(CreateParametersMixin, ModelComponent):
@@ -19,42 +17,86 @@ class Gaussian(CreateParametersMixin, ModelComponent):
If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.
Args:
- name (str): Name of the component.
area (Int, float or Parameter): Area of the Gaussian.
center (Int, float, None or Parameter): Center of the Gaussian. If None, defaults to 0 and is fixed
width (Int, float or Parameter): Standard deviation.
unit (str or sc.Unit): Unit of the parameters. Defaults to "meV".
+ display_name (str): Name of the component.
+ unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated.
"""
def __init__(
self,
- name: Optional[str] = "Gaussian",
- area: Optional[Union[Numeric, Parameter]] = 1.0,
- center: Optional[Union[Numeric, Parameter, None]] = None,
- width: Optional[Union[Numeric, Parameter]] = 1.0,
- unit: Optional[Union[str, sc.Unit]] = "meV",
+ area: Numeric | Parameter = 1.0,
+ center: Numeric | Parameter | None = None,
+ width: Numeric | Parameter = 1.0,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "Gaussian",
+ unique_name: str | None = None,
):
# Validate inputs and create Parameters if not given
- self.validate_unit(unit) # lives in ModelComponent
- self._unit = unit
+ super().__init__(
+ display_name=display_name,
+ unit=unit,
+ unique_name=unique_name,
+ )
# These methods live in ValidationMixin
- area = self._create_area_parameter(area=area, name=name, unit=self._unit)
+ area = self._create_area_parameter(
+ area=area, name=display_name, unit=self._unit
+ )
center = self._create_center_parameter(
- center=center, name=name, fix_if_none=True, unit=self._unit
+ center=center, name=display_name, fix_if_none=True, unit=self._unit
)
- width = self._create_width_parameter(width=width, name=name, unit=self._unit)
-
- super().__init__(
- name=name,
- unit=unit,
- area=area,
- center=center,
- width=width,
+ width = self._create_width_parameter(
+ width=width, name=display_name, unit=self._unit
)
+ self._area = area
+ self._center = center
+ self._width = width
+
+ @property
+ def area(self) -> Parameter:
+ """Get the area parameter."""
+ return self._area
+
+ @area.setter
+ def area(self, value: Numeric) -> None:
+ """Set the area parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("area must be a number")
+ self._area.value = value
+
+ @property
+ def center(self) -> Parameter:
+ """Get the center parameter."""
+ return self._center
+
+ @center.setter
+ def center(self, value: Numeric) -> None:
+ """Set the center parameter value."""
+ if value is None:
+ value = 0.0
+ self._center.fixed = True
+ if not isinstance(value, Numeric):
+ raise TypeError("center must be a number")
+ self._center.value = value
+
+ @property
+ def width(self) -> Parameter:
+ """Get the width parameter."""
+ return self._width
+
+ @width.setter
+ def width(self, value: Numeric) -> None:
+ """Set the width parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("width must be a number")
+ self._width.value = value
+
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
"""Evaluate the Gaussian at the given x values.
If x is a scipp Variable, the unit of the Gaussian will be converted to match x.
@@ -68,4 +110,4 @@ def evaluate(
return self.area.value * normalization * np.exp(exponent)
def __repr__(self):
- return f"Gaussian(name = {self.name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
+ return f"Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py
index 7551eaf..75cce27 100644
--- a/src/easydynamics/sample_model/components/lorentzian.py
+++ b/src/easydynamics/sample_model/components/lorentzian.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import Optional, Union
-
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
@@ -10,7 +8,7 @@
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
class Lorentzian(CreateParametersMixin, ModelComponent):
@@ -19,42 +17,85 @@ class Lorentzian(CreateParametersMixin, ModelComponent):
If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.
Args:
- name (str): Name of the component.
area (Int, float or Parameter): Area of the Lorentzian.
center (Int, float, None or Parameter): Peak center. If None, defaults to 0 and is fixed.
width (Int, float or Parameter): Half Width at Half Maximum (HWHM)
unit (str or sc.Unit): Unit of the parameters. Defaults to "meV".
+ display_name (str): Display name of the component.
+ unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated.
"""
def __init__(
self,
- name: Optional[str] = "Lorentzian",
- area: Optional[Union[Numeric, Parameter]] = 1.0,
- center: Optional[Union[Numeric, Parameter, None]] = None,
- width: Optional[Union[Numeric, Parameter]] = 1.0,
- unit: Optional[Union[str, sc.Unit]] = "meV",
+ area: Numeric | Parameter = 1.0,
+ center: Numeric | Parameter | None = None,
+ width: Numeric | Parameter = 1.0,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "Lorentzian",
+ unique_name: str | None = None,
):
- # Validate inputs and create Parameters if not given
- self.validate_unit(unit)
- self._unit = unit
+ super().__init__(
+ display_name=display_name,
+ unit=unit,
+ unique_name=unique_name,
+ )
# These methods live in ValidationMixin
- area = self._create_area_parameter(area=area, name=name, unit=self._unit)
+ area = self._create_area_parameter(
+ area=area, name=display_name, unit=self._unit
+ )
center = self._create_center_parameter(
- center=center, name=name, fix_if_none=True, unit=self._unit
+ center=center, name=display_name, fix_if_none=True, unit=self._unit
)
- width = self._create_width_parameter(width=width, name=name, unit=self._unit)
-
- super().__init__(
- name=name,
- unit=unit,
- area=area,
- center=center,
- width=width,
+ width = self._create_width_parameter(
+ width=width, name=display_name, unit=self._unit
)
+ self._area = area
+ self._center = center
+ self._width = width
+
+ @property
+ def area(self) -> Parameter:
+ """Get the area parameter."""
+ return self._area
+
+ @area.setter
+ def area(self, value: Numeric) -> None:
+ """Set the area parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("area must be a number")
+ self._area.value = value
+
+ @property
+ def center(self) -> Parameter:
+ """Get the center parameter."""
+ return self._center
+
+ @center.setter
+ def center(self, value: Numeric | None) -> None:
+ """Set the center parameter value."""
+ if value is None:
+ value = 0.0
+ self._center.fixed = True
+ if not isinstance(value, Numeric):
+ raise TypeError("center must be a number")
+ self._center.value = value
+
+ @property
+ def width(self) -> Parameter:
+ """Get the width parameter."""
+ return self._width
+
+ @width.setter
+ def width(self, value: Numeric) -> None:
+ """Set the width parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("width must be a number")
+ self._width.value = value
+
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
"""Evaluate the Lorentzian at the given x values.
If x is a scipp Variable, the unit of the Lorentzian will be converted to match x.
@@ -68,4 +109,4 @@ def evaluate(
return self.area.value * normalization / denominator
def __repr__(self):
- return f"Lorentzian(name = {self.name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
+ return f"Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n width = {self.width})"
diff --git a/src/easydynamics/sample_model/components/mixins.py b/src/easydynamics/sample_model/components/mixins.py
index 84ae9e6..f229f20 100644
--- a/src/easydynamics/sample_model/components/mixins.py
+++ b/src/easydynamics/sample_model/components/mixins.py
@@ -1,11 +1,15 @@
import warnings
-from typing import Union
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
-Numeric = Union[int, float]
+Numeric = int | float
+
+
+MINIMUM_WIDTH = 1e-10 # To avoid division by zero
+MINIMUM_AREA = 0.0 # To avoid negative areas
+DHO_MINIMUM_CENTER = 1e-10 # To avoid zero center in DHO
class CreateParametersMixin:
@@ -15,14 +19,11 @@ class CreateParametersMixin:
(area, center, width) with appropriate bounds and type checking.
"""
- MINIMUM_WIDTH = 1e-10 # To avoid division by zero
- MINIMUM_AREA = 0.0 # To avoid negative areas
-
def _create_area_parameter(
self,
- area: Union[Numeric, Parameter],
+ area: Numeric | Parameter,
name: str,
- unit: Union[str, sc.Unit] = "meV",
+ unit: str | sc.Unit = "meV",
minimum_area: float = MINIMUM_AREA,
) -> Parameter:
"""Validate and convert a number to a Parameter describing the area
@@ -60,10 +61,11 @@ def _create_area_parameter(
def _create_center_parameter(
self,
- center: Union[Numeric, Parameter, None],
+ center: Numeric | Parameter | None,
name: str,
fix_if_none: bool,
- unit: Union[str, sc.Unit] = "meV",
+ unit: str | sc.Unit = "meV",
+ enforce_minimum_center: bool = False,
) -> Parameter:
"""Validate and convert a number to a Parameter describing the center of a function.
args:
@@ -91,15 +93,16 @@ def _create_center_parameter(
raise ValueError("center must be None, a finite number or a Parameter")
center = Parameter(name=name + " center", value=float(center), unit=unit)
-
+ if enforce_minimum_center:
+ center.min = DHO_MINIMUM_CENTER
return center
def _create_width_parameter(
self,
- width: Union[Numeric, Parameter],
+ width: Numeric | Parameter,
name: str,
param_name: str = "width",
- unit: Union[str, sc.Unit] = "meV",
+ unit: str | sc.Unit = "meV",
minimum_width: float = MINIMUM_WIDTH,
) -> Parameter:
"""Validate and convert a number to a Parameter describing the width of a function.
diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py
index d6fecf6..412b265 100644
--- a/src/easydynamics/sample_model/components/model_component.py
+++ b/src/easydynamics/sample_model/components/model_component.py
@@ -2,29 +2,29 @@
import warnings
from abc import abstractmethod
-from typing import Any, List, Optional, Union
+from typing import List
import numpy as np
import scipp as sc
-from easyscience.base_classes import ObjBase
+from easyscience.base_classes.model_base import ModelBase
from scipp import UnitError
-Numeric = Union[float, int]
+Numeric = float | int
-class ModelComponent(ObjBase):
+class ModelComponent(ModelBase):
"""
Abstract base class for all model components.
"""
def __init__(
self,
- name="ModelComponent",
- unit: Optional[Union[str, sc.Unit]] = "meV",
- **kwargs: Any,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = None,
+ unique_name: str | None = None,
):
self.validate_unit(unit)
- super().__init__(name=name, **kwargs)
+ super().__init__(display_name=display_name, unique_name=unique_name)
self._unit = unit
@property
@@ -48,17 +48,17 @@ def unit(self, unit_str: str) -> None:
def fix_all_parameters(self):
"""Fix all parameters in the model component."""
- pars = self.get_parameters()
+ pars = self.get_fittable_parameters()
for p in pars:
p.fixed = True
def free_all_parameters(self):
"""Free all parameters in the model component."""
- for p in self.get_parameters():
+ for p in self.get_fittable_parameters():
p.fixed = False
def _prepare_x_for_evaluate(
- self, x: Union[Numeric, List[Numeric], np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
""" "Prepare the input x for evaluation by handling units and converting to a numpy array."""
@@ -118,7 +118,7 @@ def validate_unit(unit) -> None:
f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}"
)
- def convert_unit(self, unit: Union[str, sc.Unit]):
+ def convert_unit(self, unit: str | sc.Unit):
"""
Convert the unit of the Parameters in the component.
@@ -127,7 +127,7 @@ def convert_unit(self, unit: Union[str, sc.Unit]):
"""
old_unit = self._unit
- pars = self.get_parameters()
+ pars = self.get_all_parameters()
try:
for p in pars:
p.convert_unit(unit)
@@ -143,12 +143,12 @@ def convert_unit(self, unit: Union[str, sc.Unit]):
raise e
@abstractmethod
- def evaluate(self, x: Union[Numeric, sc.Variable]) -> np.ndarray:
+ def evaluate(self, x: Numeric | sc.Variable) -> np.ndarray:
"""
Evaluate the model component at input x.
Args:
- x (Union[Numeric, sc.Variable]): Input values.
+ x (Numeric | sc.Variable): Input values.
Returns:
np.ndarray: Evaluated function values.
@@ -156,4 +156,4 @@ def evaluate(self, x: Union[Numeric, sc.Variable]) -> np.ndarray:
pass
def __repr__(self):
- return f"{self.__class__.__name__}(name={self.name})"
+ return f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})"
diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py
index 226c4ea..c2e8856 100644
--- a/src/easydynamics/sample_model/components/polynomial.py
+++ b/src/easydynamics/sample_model/components/polynomial.py
@@ -1,16 +1,16 @@
from __future__ import annotations
import warnings
-from typing import Optional, Sequence, Union
+from typing import Sequence
import numpy as np
import scipp as sc
-from easyscience.variable import Parameter
+from easyscience.variable import DescriptorBase, Parameter
from scipp import UnitError
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
class Polynomial(ModelComponent):
@@ -20,18 +20,18 @@ class Polynomial(ModelComponent):
Args:
coefficients (list or tuple): Coefficients c0, c1, ..., cN
representing f(x) = c0 + c1*x + c2*x^2 + ... + cN*x^N
- """
+ unit (str or sc.Unit): Unit of the Polynomial component.
+ display_name (str): Display name of the Polynomial component.
+ unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated."""
def __init__(
self,
- name: Optional[str] = "Polynomial",
- coefficients: Optional[Sequence[Union[Numeric, Parameter]]] = (0.0,),
- unit: Union[str, sc.Unit] = "meV",
+ coefficients: Sequence[Numeric | Parameter] = (0.0,),
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "Polynomial",
+ unique_name: str | None = None,
):
- self.validate_unit(unit)
-
- if coefficients is None:
- raise ValueError("At least one coefficient must be provided.")
+ super().__init__(display_name=display_name, unit=unit, unique_name=unique_name)
if not isinstance(coefficients, (list, tuple, np.ndarray)):
raise TypeError(
@@ -49,7 +49,7 @@ def __init__(
if isinstance(coef, Parameter):
param = coef
elif isinstance(coef, Numeric):
- param = Parameter(name=f"{name}_c{i}", value=float(coef))
+ param = Parameter(name=f"{display_name}_c{i}", value=float(coef))
else:
raise TypeError(
"Each coefficient must be either a numeric value or a Parameter."
@@ -59,17 +59,13 @@ def __init__(
# Helper scipp scalar to track unit conversions (value initialized to 1 with provided unit)
self._unit_conversion_helper = sc.scalar(value=1.0, unit=unit)
- # call parent with the Parameters
- super().__init__(name=name, unit=unit, coefficients=self._coefficients)
-
@property
- def coefficient_values(self) -> list[float]:
- """Get the coefficients of the polynomial as a list."""
- coefficient_list = [param.value for param in self._coefficients]
- return coefficient_list
+ def coefficients(self) -> list[Parameter]:
+ """Get the coefficients of the polynomial as a list of Parameters."""
+ return list(self._coefficients)
- @coefficient_values.setter
- def coefficient_values(self, coeffs: Sequence[Union[Numeric, Parameter]]) -> None:
+ @coefficients.setter
+ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None:
"""Replace the coefficients. Length must match current number of coefficients."""
if not isinstance(coeffs, (list, tuple, np.ndarray)):
raise TypeError(
@@ -90,8 +86,13 @@ def coefficient_values(self, coeffs: Sequence[Union[Numeric, Parameter]]) -> Non
"Each coefficient must be either a numeric value or a Parameter."
)
+ def coefficient_values(self) -> list[float]:
+ """Get the coefficients of the polynomial as a list."""
+ coefficient_list = [param.value for param in self._coefficients]
+ return coefficient_list
+
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
"""Evaluate the Polynomial at the given x values.
The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
@@ -105,9 +106,9 @@ def evaluate(
if any(result < 0):
warnings.warn(
- "The Polynomial with name {} has negative values, which may not be physically meaningful.".format(
- self.name
- )
+ f"The Polynomial with unique_name {self.unique_name} has negative values, "
+ "which may not be physically meaningful.",
+ UserWarning,
)
return result
@@ -122,15 +123,15 @@ def degree(self, value: int) -> None:
"The degree of the polynomial is determined by the number of coefficients and cannot be set directly."
)
- def get_parameters(self) -> list[Parameter]:
+ def get_all_variables(self) -> list[DescriptorBase]:
"""
Get all parameters from the model component.
Returns:
List[Parameter]: List of parameters in the component.
"""
- return self._coefficients
+ return list(self._coefficients)
- def convert_unit(self, unit: Union[str, sc.Unit]):
+ def convert_unit(self, unit: str | sc.Unit):
"""Convert the unit of the polynomial.
Args:
unit (str or sc.Unit): The target unit to convert to.
@@ -156,7 +157,7 @@ def __repr__(self) -> str:
coeffs_str = ", ".join(
f"{param.name}={param.value}" for param in self._coefficients
)
- return f"Polynomial(name = {self.name}, unit = {self._unit},\n coefficients = [{coeffs_str}])"
+ return f"Polynomial(unique_name = {self.unique_name}, unit = {self._unit},\n coefficients = [{coeffs_str}])"
# from typing import Callable, Dict
diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py
index 74e1d57..0adfafc 100644
--- a/src/easydynamics/sample_model/components/voigt.py
+++ b/src/easydynamics/sample_model/components/voigt.py
@@ -1,7 +1,5 @@
from __future__ import annotations
-from typing import Optional, Union
-
import numpy as np
import scipp as sc
from easyscience.variable import Parameter
@@ -11,7 +9,7 @@
from .model_component import ModelComponent
-Numeric = Union[float, int]
+Numeric = float | int
class Voigt(CreateParametersMixin, ModelComponent):
@@ -20,56 +18,109 @@ class Voigt(CreateParametersMixin, ModelComponent):
If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.
Args:
- name (str): Name of the component.
+ area (Int or float): Total area under the curve.
center (Int or float or None): Center of the Voigt profile.
gaussian_width (Int or float): Standard deviation of the Gaussian part.
lorentzian_width (Int or float): Half width at half max (HWHM) of the Lorentzian part.
- area (Int or float): Total area under the curve.
unit (str or sc.Unit): Unit of the parameters. Defaults to "meV".
+ display_name (str): Display name of the component.
+ unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated.
"""
def __init__(
self,
- name: Optional[str] = "Voigt",
- area: Optional[Union[Numeric, Parameter]] = 1.0,
- center: Optional[Union[Numeric, Parameter, None]] = None,
- gaussian_width: Optional[Union[Numeric, Parameter]] = 1.0,
- lorentzian_width: Optional[Union[Numeric, Parameter]] = 1.0,
- unit: Optional[Union[str, sc.Unit]] = "meV",
+ area: Numeric | Parameter = 1.0,
+ center: Numeric | Parameter | None = None,
+ gaussian_width: Numeric | Parameter = 1.0,
+ lorentzian_width: Numeric | Parameter = 1.0,
+ unit: str | sc.Unit = "meV",
+ display_name: str | None = "Voigt",
+ unique_name: str | None = None,
):
- # Validate inputs and create Parameters if not given
- self.validate_unit(unit)
- self._unit = unit
+ super().__init__(
+ display_name=display_name,
+ unit=unit,
+ unique_name=unique_name,
+ )
# These methods live in ValidationMixin
- area = self._create_area_parameter(area=area, name=name, unit=self._unit)
+ area = self._create_area_parameter(
+ area=area, name=display_name, unit=self._unit
+ )
center = self._create_center_parameter(
- center=center, name=name, fix_if_none=True, unit=self._unit
+ center=center, name=display_name, fix_if_none=True, unit=self._unit
)
gaussian_width = self._create_width_parameter(
width=gaussian_width,
- name=name,
+ name=display_name,
param_name="gaussian_width",
unit=self._unit,
)
lorentzian_width = self._create_width_parameter(
width=lorentzian_width,
- name=name,
+ name=display_name,
param_name="lorentzian_width",
unit=self._unit,
)
- super().__init__(
- name=name,
- unit=unit,
- area=area,
- center=center,
- gaussian_width=gaussian_width,
- lorentzian_width=lorentzian_width,
- )
+ self._area = area
+ self._center = center
+ self._gaussian_width = gaussian_width
+ self._lorentzian_width = lorentzian_width
+
+ @property
+ def area(self) -> Parameter:
+ """Get the area parameter."""
+ return self._area
+
+ @area.setter
+ def area(self, value: Numeric) -> None:
+ """Set the area parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("area must be a number")
+ self._area.value = value
+
+ @property
+ def center(self) -> Parameter:
+ """Get the center parameter."""
+ return self._center
+
+ @center.setter
+ def center(self, value: Numeric | None) -> None:
+ """Set the center parameter value."""
+ if value is None:
+ value = 0.0
+ self._center.fixed = True
+ if not isinstance(value, Numeric):
+ raise TypeError("center must be a number")
+ self._center.value = value
+
+ @property
+ def gaussian_width(self) -> Parameter:
+ """Get the width parameter."""
+ return self._gaussian_width
+
+ @gaussian_width.setter
+ def gaussian_width(self, value: Numeric) -> None:
+ """Set the width parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("gaussian_width must be a number")
+ self._gaussian_width.value = value
+
+ @property
+ def lorentzian_width(self) -> Parameter:
+ """Get the width parameter."""
+ return self._lorentzian_width
+
+ @lorentzian_width.setter
+ def lorentzian_width(self, value: Numeric) -> None:
+ """Set the width parameter value."""
+ if not isinstance(value, Numeric):
+ raise TypeError("lorentzian_width must be a number")
+ self._lorentzian_width.value = value
def evaluate(
- self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray]
+ self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
"""Evaluate the Voigt at the given x values.
If x is a scipp Variable, the unit of the Voigt will be converted to match x.
@@ -84,4 +135,4 @@ def evaluate(
)
def __repr__(self):
- return f"Voigt(name = {self.name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n gaussian_width = {self.gaussian_width},\n lorentzian_width = {self.lorentzian_width})"
+ return f"Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n area = {self.area},\n center = {self.center},\n gaussian_width = {self.gaussian_width},\n lorentzian_width = {self.lorentzian_width})"
diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py
index dcca2e2..311768c 100644
--- a/src/easydynamics/utils/detailed_balance.py
+++ b/src/easydynamics/utils/detailed_balance.py
@@ -1,5 +1,4 @@
import warnings
-from typing import Optional, Union
import numpy as np
import scipp as sc
@@ -13,10 +12,10 @@
def _detailed_balance_factor(
- energy: Union[int, float, list, np.ndarray, sc.Variable],
- temperature: Union[int, float, sc.Variable, Parameter],
- energy_unit: Union[str, sc.Unit] = "meV",
- temperature_unit: Union[str, sc.Unit] = "K",
+ energy: int | float | list | np.ndarray | sc.Variable,
+ temperature: int | float | sc.Variable | Parameter,
+ energy_unit: str | sc.Unit = "meV",
+ temperature_unit: str | sc.Unit = "K",
divide_by_temperature: bool = True,
) -> np.ndarray:
"""
@@ -141,9 +140,9 @@ def _detailed_balance_factor(
def _convert_to_scipp_variable(
- value: Union[int, float, list, np.ndarray, Parameter, sc.Variable],
+ value: int | float | list | np.ndarray | Parameter | sc.Variable,
name: str,
- unit: Optional[str] = None,
+ unit: str | None = None,
) -> sc.Variable:
"""Convert various input types to a scipp Variable with proper units."""
if isinstance(value, sc.Variable):
diff --git a/tests/unit_tests/sample_model/components/test_damped_harmonic_oscillator.py b/tests/unit_tests/sample_model/components/test_damped_harmonic_oscillator.py
index 6415ce4..2bc8aa4 100644
--- a/tests/unit_tests/sample_model/components/test_damped_harmonic_oscillator.py
+++ b/tests/unit_tests/sample_model/components/test_damped_harmonic_oscillator.py
@@ -12,7 +12,7 @@ class TestDampedHarmonicOscillator:
@pytest.fixture
def dho(self):
return DampedHarmonicOscillator(
- name="TestDHO", area=2.0, center=1.5, width=0.3, unit="meV"
+ display_name="TestDHO", area=2.0, center=1.5, width=0.3, unit="meV"
)
def test_init_no_inputs(self):
@@ -20,7 +20,7 @@ def test_init_no_inputs(self):
dho = DampedHarmonicOscillator()
# EXPECT
- assert dho.name == "DampedHarmonicOscillator"
+ assert dho.display_name == "DampedHarmonicOscillator"
assert dho.area.value == 1.0
assert dho.center.value == 1.0
assert dho.width.value == 1.0
@@ -28,7 +28,7 @@ def test_init_no_inputs(self):
def test_initialization(self, dho: DampedHarmonicOscillator):
# WHEN THEN EXPECT
- assert dho.name == "TestDHO"
+ assert dho.display_name == "TestDHO"
assert dho.area.value == 2.0
assert dho.center.value == 1.5
assert dho.width.value == 0.3
@@ -42,7 +42,7 @@ def test_init_with_parameters(self):
# THEN
dho = DampedHarmonicOscillator(
- name="Paramdho",
+ display_name="Paramdho",
area=area_param,
center=center_param,
width=width_param,
@@ -50,7 +50,7 @@ def test_init_with_parameters(self):
)
# EXPECT
- assert dho.name == "Paramdho"
+ assert dho.display_name == "Paramdho"
assert dho.area is area_param
assert dho.center is center_param
assert dho.width is width_param
@@ -79,7 +79,7 @@ def test_init_with_parameters(self):
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- DampedHarmonicOscillator(name="DampedHarmonicOscillator", **kwargs)
+ DampedHarmonicOscillator(display_name="DampedHarmonicOscillator", **kwargs)
def test_negative_width_raises(self):
# WHEN THEN EXPECT
@@ -88,7 +88,7 @@ def test_negative_width_raises(self):
match="The width of a DampedHarmonicOscillator must be greater than zero.",
):
DampedHarmonicOscillator(
- name="TestDampedHarmonicOscillator",
+ display_name="TestDampedHarmonicOscillator",
area=2.0,
center=0.5,
width=-0.6,
@@ -99,7 +99,7 @@ def test_negative_area_warns(self):
# WHEN THEN EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
DampedHarmonicOscillator(
- name="TestDampedHarmonicOscillator",
+ display_name="TestDampedHarmonicOscillator",
area=-2.0,
center=0.5,
width=0.6,
@@ -148,9 +148,9 @@ def test_evaluate(self, dho: DampedHarmonicOscillator):
)
np.testing.assert_allclose(result, expected_result, rtol=1e-5)
- def test_get_parameters(self, dho: DampedHarmonicOscillator):
+ def test_get_all_parameters(self, dho: DampedHarmonicOscillator):
# WHEN THEN
- params = dho.get_parameters()
+ params = dho.get_all_parameters()
# EXPECT
assert len(params) == 3
@@ -192,7 +192,7 @@ def test_copy(self, dho: DampedHarmonicOscillator):
# EXPECT
assert dho_copy is not dho
- assert dho_copy.name == dho.name
+ assert dho_copy.display_name == dho.display_name
assert dho_copy.area.value == dho.area.value
assert dho_copy.area.fixed == dho.area.fixed
diff --git a/tests/unit_tests/sample_model/components/test_delta_function.py b/tests/unit_tests/sample_model/components/test_delta_function.py
index 9a78c06..f944ae3 100644
--- a/tests/unit_tests/sample_model/components/test_delta_function.py
+++ b/tests/unit_tests/sample_model/components/test_delta_function.py
@@ -12,14 +12,16 @@
class TestDeltaFunction:
@pytest.fixture
def delta_function(self):
- return DeltaFunction(name="TestDeltaFunction", area=2.0, center=0.5, unit="meV")
+ return DeltaFunction(
+ display_name="TestDeltaFunction", area=2.0, center=0.5, unit="meV"
+ )
def test_init_no_inputs(self):
# WHEN THEN
delta_function = DeltaFunction()
# EXPECT
- assert delta_function.name == "DeltaFunction"
+ assert delta_function.display_name == "DeltaFunction"
assert delta_function.area.value == 1.0
assert delta_function.center.value == 0.0
assert delta_function.unit == "meV"
@@ -27,7 +29,7 @@ def test_init_no_inputs(self):
def test_initialization(self, delta_function: DeltaFunction):
# WHEN THEN EXPECT
- assert delta_function.name == "TestDeltaFunction"
+ assert delta_function.display_name == "TestDeltaFunction"
assert delta_function.area.value == 2.0
assert delta_function.center.value == 0.5
assert delta_function.unit == "meV"
@@ -51,12 +53,14 @@ def test_initialization(self, delta_function: DeltaFunction):
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- DeltaFunction(name="TestDeltaFunction", **kwargs)
+ DeltaFunction(display_name="TestDeltaFunction", **kwargs)
def test_negative_area_warns(self):
# WHEN THEN EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
- DeltaFunction(name="TestDeltaFunction", area=-2.0, center=0.5, unit="meV")
+ DeltaFunction(
+ display_name="TestDeltaFunction", area=-2.0, center=0.5, unit="meV"
+ )
@pytest.mark.parametrize(
"prop, valid_value, invalid_value, invalid_message",
@@ -163,16 +167,16 @@ def test_evaluate_with_invalid_input_raises(
def test_center_is_fixed_if_set_to_None(self):
# WHEN THEN
test_delta = DeltaFunction(
- name="TestDeltaFunction", area=2.0, center=None, unit="meV"
+ display_name="TestDeltaFunction", area=2.0, center=None, unit="meV"
)
# EXPECT
assert test_delta.center.value == 0.0
assert test_delta.center.fixed is True
- def test_get_parameters(self, delta_function: DeltaFunction):
+ def test_get_all_parameters(self, delta_function: DeltaFunction):
# WHEN THEN
- params = delta_function.get_parameters()
+ params = delta_function.get_all_parameters()
# EXPECT
assert len(params) == 2
@@ -199,7 +203,7 @@ def test_copy(self, delta_function: DeltaFunction):
# EXPECT
assert delta_copy is not delta_function
- assert delta_copy.name == delta_function.name
+ assert delta_copy.display_name == delta_function.display_name
assert delta_copy.area.value == delta_function.area.value
assert delta_copy.area.fixed == delta_function.area.fixed
@@ -215,7 +219,7 @@ def test_repr(self, delta_function: DeltaFunction):
# EXPECT
assert "DeltaFunction" in repr_str
- assert "name = TestDeltaFunction" in repr_str
+ assert "unique_name = DeltaFunction" in repr_str
assert "unit = meV" in repr_str
assert "area =" in repr_str
assert "center =" in repr_str
diff --git a/tests/unit_tests/sample_model/components/test_gaussian.py b/tests/unit_tests/sample_model/components/test_gaussian.py
index faffb31..a8fc771 100644
--- a/tests/unit_tests/sample_model/components/test_gaussian.py
+++ b/tests/unit_tests/sample_model/components/test_gaussian.py
@@ -12,7 +12,7 @@ class TestGaussian:
@pytest.fixture
def gaussian(self):
return Gaussian(
- name="TestGaussian", area=2.0, center=0.5, width=0.6, unit="meV"
+ display_name="TestGaussian", area=2.0, center=0.5, width=0.6, unit="meV"
)
def test_init_no_inputs(self):
@@ -20,7 +20,7 @@ def test_init_no_inputs(self):
gaussian = Gaussian()
# EXPECT
- assert gaussian.name == "Gaussian"
+ assert gaussian.display_name == "Gaussian"
assert gaussian.area.value == 1.0
assert gaussian.center.value == 0.0
assert gaussian.width.value == 1.0
@@ -29,7 +29,7 @@ def test_init_no_inputs(self):
def test_initialization(self, gaussian: Gaussian):
# WHEN THEN EXPECT
- assert gaussian.name == "TestGaussian"
+ assert gaussian.display_name == "TestGaussian"
assert gaussian.area.value == 2.0
assert gaussian.center.value == 0.5
assert gaussian.width.value == 0.6
@@ -43,7 +43,7 @@ def test_init_with_parameters(self):
# THEN
gaussian = Gaussian(
- name="ParamGaussian",
+ display_name="ParamGaussian",
area=area_param,
center=center_param,
width=width_param,
@@ -51,7 +51,7 @@ def test_init_with_parameters(self):
)
# EXPECT
- assert gaussian.name == "ParamGaussian"
+ assert gaussian.display_name == "ParamGaussian"
assert gaussian.area is area_param
assert gaussian.center is center_param
assert gaussian.width is width_param
@@ -80,19 +80,31 @@ def test_init_with_parameters(self):
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- Gaussian(name="TestGaussian", **kwargs)
+ Gaussian(display_name="TestGaussian", **kwargs)
def test_negative_width_raises(self):
# WHEN THEN EXPECT
with pytest.raises(
ValueError, match="The width of a Gaussian must be greater than zero."
):
- Gaussian(name="TestGaussian", area=2.0, center=0.5, width=-0.6, unit="meV")
+ Gaussian(
+ display_name="TestGaussian",
+ area=2.0,
+ center=0.5,
+ width=-0.6,
+ unit="meV",
+ )
def test_negative_area_warns(self):
# WHEN THEN EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
- Gaussian(name="TestGaussian", area=-2.0, center=0.5, width=0.6, unit="meV")
+ Gaussian(
+ display_name="TestGaussian",
+ area=-2.0,
+ center=0.5,
+ width=0.6,
+ unit="meV",
+ )
@pytest.mark.parametrize(
"prop, valid_value, invalid_value, invalid_message",
@@ -129,15 +141,15 @@ def test_evaluate(self, gaussian: Gaussian):
def test_center_is_fixed_if_set_to_None(self):
# WHEN THEN
test_gaussian = Gaussian(
- name="TestGaussian", area=2.0, center=None, width=0.6, unit="meV"
+ display_name="TestGaussian", area=2.0, center=None, width=0.6, unit="meV"
)
# EXPECT
assert test_gaussian.center.value == 0.0
assert test_gaussian.center.fixed is True
- def test_get_parameters(self, gaussian: Gaussian):
+ def test_get_all_parameters(self, gaussian: Gaussian):
# WHEN THEN
- params = gaussian.get_parameters()
+ params = gaussian.get_all_parameters()
# EXPECT
assert len(params) == 3
@@ -181,7 +193,7 @@ def test_copy(self, gaussian: Gaussian):
gaussian_copy = copy(gaussian)
# EXPECT
assert gaussian_copy is not gaussian
- assert gaussian_copy.name == gaussian.name
+ assert gaussian_copy.display_name == gaussian.display_name
assert gaussian_copy.area.value == gaussian.area.value
assert gaussian_copy.area.fixed == gaussian.area.fixed
@@ -199,7 +211,7 @@ def test_repr(self, gaussian: Gaussian):
repr_str = repr(gaussian)
# EXPECT
assert "Gaussian" in repr_str
- assert "name = TestGaussian" in repr_str
+ assert "unique_name = Gaussian" in repr_str
assert "unit = meV" in repr_str
assert "area =" in repr_str
assert "center =" in repr_str
diff --git a/tests/unit_tests/sample_model/components/test_lorentzian.py b/tests/unit_tests/sample_model/components/test_lorentzian.py
index 43c73b8..3a60821 100644
--- a/tests/unit_tests/sample_model/components/test_lorentzian.py
+++ b/tests/unit_tests/sample_model/components/test_lorentzian.py
@@ -12,7 +12,7 @@ class TestLorentzian:
@pytest.fixture
def lorentzian(self):
return Lorentzian(
- name="TestLorentzian", area=2.0, center=0.5, width=0.6, unit="meV"
+ display_name="TestLorentzian", area=2.0, center=0.5, width=0.6, unit="meV"
)
def test_init_no_inputs(self):
@@ -20,7 +20,7 @@ def test_init_no_inputs(self):
lorentzian = Lorentzian()
# EXPECT
- assert lorentzian.name == "Lorentzian"
+ assert lorentzian.display_name == "Lorentzian"
assert lorentzian.area.value == 1.0
assert lorentzian.center.value == 0.0
assert lorentzian.width.value == 1.0
@@ -29,7 +29,7 @@ def test_init_no_inputs(self):
def test_initialization(self, lorentzian: Lorentzian):
# WHEN THEN EXPECT
- assert lorentzian.name == "TestLorentzian"
+ assert lorentzian.display_name == "TestLorentzian"
assert lorentzian.area.value == 2.0
assert lorentzian.center.value == 0.5
assert lorentzian.width.value == 0.6
@@ -43,7 +43,7 @@ def test_init_with_parameters(self):
# THEN
lorentzian = Lorentzian(
- name="ParamLorentzian",
+ display_name="ParamLorentzian",
area=area_param,
center=center_param,
width=width_param,
@@ -51,7 +51,7 @@ def test_init_with_parameters(self):
)
# EXPECT
- assert lorentzian.name == "ParamLorentzian"
+ assert lorentzian.display_name == "ParamLorentzian"
assert lorentzian.area is area_param
assert lorentzian.center is center_param
assert lorentzian.width is width_param
@@ -80,7 +80,7 @@ def test_init_with_parameters(self):
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- Lorentzian(name="TestLorentzian", **kwargs)
+ Lorentzian(display_name="TestLorentzian", **kwargs)
def test_negative_width_raises(self):
# WHEN THEN EXPECT
@@ -88,14 +88,22 @@ def test_negative_width_raises(self):
ValueError, match="The width of a Lorentzian must be greater than zero."
):
Lorentzian(
- name="TestLorentzian", area=2.0, center=0.5, width=-0.6, unit="meV"
+ display_name="TestLorentzian",
+ area=2.0,
+ center=0.5,
+ width=-0.6,
+ unit="meV",
)
def test_negative_area_warns(self):
# WHEN THEN EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
Lorentzian(
- name="TestLorentzian", area=-2.0, center=0.5, width=0.6, unit="meV"
+ display_name="TestLorentzian",
+ area=-2.0,
+ center=0.5,
+ width=0.6,
+ unit="meV",
)
@pytest.mark.parametrize(
@@ -131,16 +139,16 @@ def test_evaluate(self, lorentzian: Lorentzian):
def test_center_is_fixed_if_set_to_None(self):
# WHEN THEN
test_lorentzian = Lorentzian(
- name="TestLorentzian", area=2.0, center=None, width=0.6, unit="meV"
+ display_name="TestLorentzian", area=2.0, center=None, width=0.6, unit="meV"
)
# EXPECT
assert test_lorentzian.center.value == 0.0
assert test_lorentzian.center.fixed is True
- def test_get_parameters(self, lorentzian: Lorentzian):
+ def test_get_all_parameters(self, lorentzian: Lorentzian):
# WHEN THEN
- params = lorentzian.get_parameters()
+ params = lorentzian.get_all_parameters()
# EXPECT
assert len(params) == 3
@@ -182,7 +190,7 @@ def test_copy(self, lorentzian: Lorentzian):
# EXPECT
assert lorentzian_copy is not lorentzian
- assert lorentzian_copy.name == lorentzian.name
+ assert lorentzian_copy.display_name == lorentzian.display_name
assert lorentzian_copy.area.value == lorentzian.area.value
assert lorentzian_copy.area.fixed == lorentzian.area.fixed
@@ -201,7 +209,7 @@ def test_repr(self, lorentzian: Lorentzian):
# EXPECT
assert "Lorentzian" in repr_str
- assert "name = TestLorentzian" in repr_str
+ assert "unique_name = Lorentzian" in repr_str
assert "unit = meV" in repr_str
assert "area =" in repr_str
assert "center =" in repr_str
diff --git a/tests/unit_tests/sample_model/components/test_model_component.py b/tests/unit_tests/sample_model/components/test_model_component.py
index cd3776e..ba1ea4d 100644
--- a/tests/unit_tests/sample_model/components/test_model_component.py
+++ b/tests/unit_tests/sample_model/components/test_model_component.py
@@ -5,16 +5,18 @@
from easydynamics.sample_model.components.model_component import ModelComponent
+Numeric = float | int
+
class DummyComponent(ModelComponent):
def __init__(self):
- super().__init__(name="Dummy")
+ super().__init__(display_name="Dummy")
self.area = Parameter(name="area", value=1.0, unit="meV", fixed=False)
self.center = Parameter(name="center", value=2.0, unit="meV", fixed=True)
self.width = Parameter(name="width", value=3.0, unit="meV", fixed=True)
self._unit = "meV"
- def get_parameters(self):
+ def get_all_parameters(self):
return [self.area, self.center, self.width]
def evaluate(self, x):
@@ -44,11 +46,11 @@ def test_convert_unit(self, dummy: DummyComponent):
def test_free_and_fix_all_parameters(self, dummy):
# WHEN THEN EXPECT
dummy.free_all_parameters()
- assert all(not p.fixed for p in dummy.get_parameters())
+ assert all(not p.fixed for p in dummy.get_all_parameters())
# THEN EXPECT
dummy.fix_all_parameters()
- assert all(p.fixed for p in dummy.get_parameters())
+ assert all(p.fixed for p in dummy.get_all_parameters())
def test_repr(self, dummy):
# WHEN THEN EXPECT
diff --git a/tests/unit_tests/sample_model/components/test_polynomial.py b/tests/unit_tests/sample_model/components/test_polynomial.py
index 14ba18f..ed83093 100644
--- a/tests/unit_tests/sample_model/components/test_polynomial.py
+++ b/tests/unit_tests/sample_model/components/test_polynomial.py
@@ -10,20 +10,20 @@
class TestPolynomial:
@pytest.fixture
def polynomial(self):
- return Polynomial(name="TestPolynomial", coefficients=[1.0, -2.0, 3.0])
+ return Polynomial(display_name="TestPolynomial", coefficients=[1.0, -2.0, 3.0])
def test_init_no_inputs(self):
# WHEN THEN
polynomial = Polynomial()
# EXPECT
- assert polynomial.name == "Polynomial"
+ assert polynomial.display_name == "Polynomial"
assert polynomial.coefficients[0].value == 0.0
assert polynomial.unit == "meV"
def test_initialization(self, polynomial: Polynomial):
# WHEN THEN EXPECT
- assert polynomial.name == "TestPolynomial"
+ assert polynomial.display_name == "TestPolynomial"
assert polynomial.coefficients[0].value == 1.0
assert polynomial.coefficients[1].value == -2.0
assert polynomial.coefficients[2].value == 3.0
@@ -43,24 +43,21 @@ def test_initialization(self, polynomial: Polynomial):
{"coefficients": [1.0, -2.0, 3.0], "unit": 123},
"unit must be ",
),
+ (
+ {"coefficients": None},
+ "coefficients must be ",
+ ),
],
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- Polynomial(name="TestPolynomial", **kwargs)
-
- @pytest.mark.parametrize("invalid_coeffs", [[], None])
- def test_no_coefficients_raises(self, invalid_coeffs):
- # WHEN THEN EXPECT
- with pytest.raises(
- ValueError, match="At least one coefficient must be provided"
- ):
- Polynomial(name="TestPolynomial", coefficients=invalid_coeffs)
+ Polynomial(display_name="TestPolynomial", **kwargs)
def test_negative_value_warns_in_evaluate(self):
- # WHEN THEN EXPECT
+ # WHEN THEN
+ test_polynomial = Polynomial(display_name="TestPolynomial", coefficients=[-1.0])
+ # EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
- test_polynomial = Polynomial(name="TestPolynomial", coefficients=[-1.0])
test_polynomial.evaluate(np.array([0.0, 1.0, 2.0]))
def test_evaluate(self, polynomial: Polynomial):
@@ -90,10 +87,10 @@ def test_degree(self, polynomial: Polynomial):
[2.0, Parameter("p1", 0.0), -1.0], # mixed numbers and Parameters
],
)
- def test_set_coefficient_values(self, polynomial: Polynomial, values):
+ def test_set_coefficients(self, polynomial: Polynomial, values):
"""Test that coefficients can be updated from numeric values or Parameters."""
# WHEN
- polynomial.coefficient_values = values
+ polynomial.coefficients = values
# THEN EXPECT: Parameter values match the new inputs
for i, val in enumerate(values):
@@ -106,12 +103,12 @@ def test_set_coefficient_values(self, polynomial: Polynomial, values):
def test_set_coefficients_wrong_length_raises(self, polynomial: Polynomial):
"""Ensure that setting coefficients with mismatched length raises an error."""
with pytest.raises(ValueError, match="Number of coefficients"):
- polynomial.coefficient_values = [1.0, 2.0] # shorter list
+ polynomial.coefficients = [1.0, 2.0] # shorter list
def test_set_coefficients_invalid_type_raises(self, polynomial: Polynomial):
"""Ensure that invalid coefficient types raise a TypeError."""
with pytest.raises(TypeError):
- polynomial.coefficient_values = [1.0, "invalid", 3.0]
+ polynomial.coefficients = [1.0, "invalid", 3.0]
@pytest.mark.parametrize(
"invalid_coeffs, expected_message",
@@ -121,16 +118,21 @@ def test_set_coefficients_invalid_type_raises(self, polynomial: Polynomial):
("not a list", "coefficients must be "),
],
)
- def test_set_coefficient_values_raises(self, invalid_coeffs, expected_message):
+ def test_set_coefficients_raises(self, invalid_coeffs, expected_message):
with pytest.raises(TypeError, match=expected_message):
polynomial = Polynomial(
- name="TestPolynomial", coefficients=[1.0, -2.0, 3.0]
+ display_name="TestPolynomial", coefficients=[1.0, -2.0, 3.0]
)
- polynomial.coefficient_values = invalid_coeffs
+ polynomial.coefficients = invalid_coeffs
+
+ def test_coefficient_values(self, polynomial: Polynomial):
+ # WHEN THEN EXPECT
+ coeff_values = polynomial.coefficient_values()
+ assert coeff_values == [1.0, -2.0, 3.0]
- def test_get_parameters(self, polynomial: Polynomial):
+ def test_get_all_parameters(self, polynomial: Polynomial):
# WHEN THEN
- params = polynomial.get_parameters()
+ params = polynomial.get_all_parameters()
# EXPECT
assert len(params) == 3
@@ -159,10 +161,10 @@ def test_copy(self, polynomial: Polynomial):
# EXPECT
assert polynomial_copy is not polynomial
- assert polynomial_copy.name == polynomial.name
+ assert polynomial_copy.display_name == polynomial.display_name
assert len(polynomial_copy.coefficients) == len(polynomial.coefficients)
for original_coeff, copied_coeff in zip(
- polynomial.get_parameters(), polynomial_copy.get_parameters()
+ polynomial.get_all_parameters(), polynomial_copy.get_all_parameters()
):
assert copied_coeff.value == original_coeff.value
assert copied_coeff.fixed == original_coeff.fixed
@@ -173,5 +175,5 @@ def test_repr(self, polynomial: Polynomial):
# EXPECT
assert "Polynomial" in repr_str
- assert "name = TestPolynomial" in repr_str
+ assert "unique_name = Polynomial" in repr_str
assert "coefficients =" in repr_str
diff --git a/tests/unit_tests/sample_model/components/test_voigt.py b/tests/unit_tests/sample_model/components/test_voigt.py
index 9b59b9d..cb3caaf 100644
--- a/tests/unit_tests/sample_model/components/test_voigt.py
+++ b/tests/unit_tests/sample_model/components/test_voigt.py
@@ -13,7 +13,7 @@ class TestVoigt:
@pytest.fixture
def voigt(self):
return Voigt(
- name="TestVoigt",
+ display_name="TestVoigt",
area=2.0,
center=0.5,
gaussian_width=0.6,
@@ -26,7 +26,7 @@ def test_init_no_inputs(self):
voigt = Voigt()
# EXPECT
- assert voigt.name == "Voigt"
+ assert voigt.display_name == "Voigt"
assert voigt.area.value == 1.0
assert voigt.center.value == 0.0
assert voigt.gaussian_width.value == 1.0
@@ -36,7 +36,7 @@ def test_init_no_inputs(self):
def test_initialization(self, voigt: Voigt):
# WHEN THEN EXPECT
- assert voigt.name == "TestVoigt"
+ assert voigt.display_name == "TestVoigt"
assert voigt.area.value == 2.0
assert voigt.center.value == 0.5
assert voigt.gaussian_width.value == 0.6
@@ -56,7 +56,7 @@ def test_init_with_parameters(self):
# THEN
voigt = Voigt(
- name="ParamVoigt",
+ display_name="ParamVoigt",
area=area_param,
center=center_param,
gaussian_width=gaussian_width_param,
@@ -65,7 +65,7 @@ def test_init_with_parameters(self):
)
# EXPECT
- assert voigt.name == "ParamVoigt"
+ assert voigt.display_name == "ParamVoigt"
assert voigt.area is area_param
assert voigt.center is center_param
assert voigt.gaussian_width is gaussian_width_param
@@ -129,7 +129,7 @@ def test_init_with_parameters(self):
)
def test_input_type_validation_raises(self, kwargs, expected_message):
with pytest.raises(TypeError, match=expected_message):
- Voigt(name="TestVoigt", **kwargs)
+ Voigt(display_name="TestVoigt", **kwargs)
def test_negative_gaussian_width_raises(self):
# WHEN THEN EXPECT
@@ -137,7 +137,7 @@ def test_negative_gaussian_width_raises(self):
ValueError, match="The gaussian_width of a Voigt must be greater than."
):
Voigt(
- name="TestVoigt",
+ display_name="TestVoigt",
area=2.0,
center=0.5,
gaussian_width=-0.6,
@@ -152,7 +152,7 @@ def test_negative_lorentzian_width_raises(self):
match="The lorentzian_width of a Voigt must be greater than zero.",
):
Voigt(
- name="TestVoigt",
+ display_name="TestVoigt",
area=2.0,
center=0.5,
gaussian_width=0.6,
@@ -164,7 +164,7 @@ def test_negative_area_warns(self):
# WHEN THEN EXPECT
with pytest.warns(UserWarning, match="may not be physically meaningful"):
Voigt(
- name="TestVoigt",
+ display_name="TestVoigt",
area=-2.0,
center=0.5,
gaussian_width=0.6,
@@ -211,7 +211,7 @@ def test_evaluate(self, voigt: Voigt):
def test_center_is_fixed_if_set_to_None(self):
# WHEN THEN
test_voigt = Voigt(
- name="TestVoigt",
+ display_name="TestVoigt",
area=2.0,
center=None,
gaussian_width=0.6,
@@ -234,9 +234,9 @@ def test_convert_unit(self, voigt: Voigt):
assert voigt.gaussian_width.value == 0.6 * 1e3
assert voigt.lorentzian_width.value == 0.7 * 1e3
- def test_get_parameters(self, voigt: Voigt):
+ def test_get_all_parameters(self, voigt: Voigt):
# WHEN THEN
- params = voigt.get_parameters()
+ params = voigt.get_all_parameters()
# EXPECT
assert len(params) == 4
@@ -273,7 +273,7 @@ def test_copy(self, voigt: Voigt):
# EXPECT
assert voigt_copy is not voigt
- assert voigt_copy.name == voigt.name
+ assert voigt_copy.display_name == voigt.display_name
assert voigt_copy.area.value == voigt.area.value
assert voigt_copy.area.fixed == voigt.area.fixed
@@ -295,7 +295,7 @@ def test_repr(self, voigt: Voigt):
# EXPECT
assert "Voigt" in repr_str
- assert "name = TestVoigt" in repr_str
+ assert "unique_name = Voigt" in repr_str
assert "unit = meV" in repr_str
assert "area =" in repr_str
assert "center =" in repr_str
diff --git a/tests/unit_tests/sample_model/test_component_collection.py b/tests/unit_tests/sample_model/test_component_collection.py
new file mode 100644
index 0000000..56aede4
--- /dev/null
+++ b/tests/unit_tests/sample_model/test_component_collection.py
@@ -0,0 +1,480 @@
+from copy import copy
+
+import numpy as np
+import pytest
+from easyscience.variable import Parameter
+from scipy.integrate import simpson
+
+from easydynamics.sample_model import (
+ ComponentCollection,
+ Gaussian,
+ Lorentzian,
+ Polynomial,
+)
+
+
+class TestComponentCollection:
+ @pytest.fixture
+ def component_collection(self):
+ model = ComponentCollection(display_name="TestComponentCollection")
+ component1 = Gaussian(
+ display_name="TestGaussian1",
+ area=1.0,
+ center=0.0,
+ width=1.0,
+ unit="meV",
+ unique_name="TestGaussian1",
+ )
+ component2 = Lorentzian(
+ display_name="TestLorentzian1",
+ area=2.0,
+ center=1.0,
+ width=0.5,
+ unit="meV",
+ unique_name="TestLorentzian1",
+ )
+ model.add_component(component1)
+ model.add_component(component2)
+ return model
+
+ def test_init(self):
+ # WHEN THEN
+ component_collection = ComponentCollection(display_name="InitModel")
+
+ # EXPECT
+ assert component_collection.display_name == "InitModel"
+ assert component_collection.components == []
+
+ def test_init_with_components(self):
+ # WHEN THEN
+ component1 = Gaussian(
+ display_name="TestGaussian1", area=1.0, center=0.0, width=1.0, unit="meV"
+ )
+ component2 = Lorentzian(
+ display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, unit="meV"
+ )
+ component_collection = ComponentCollection(
+ display_name="InitModel", components=[component1, component2]
+ )
+
+ # EXPECT
+ assert component_collection.display_name == "InitModel"
+ assert len(component_collection.components) == 2
+ assert component_collection.components[0] is component1
+ assert component_collection.components[1] is component2
+
+ # ───── Component Management ─────
+
+ def test_add_component(self, component_collection):
+ # WHEN
+ component = Gaussian(
+ display_name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV"
+ )
+ # THEN
+ component_collection.add_component(component)
+ # EXPECT
+ assert component_collection.components[-1] is component
+
+ def test_add_existing_component_raises(self, component_collection):
+ # WHEN THEN
+ component = component_collection.components[0]
+ # EXPECT
+ with pytest.raises(ValueError, match="is already in the collection"):
+ component_collection.add_component(component)
+
+ def test_add_invalid_component_raises(self, component_collection):
+ # WHEN THEN EXPECT
+ with pytest.raises(
+ TypeError, match="Component must be an instance of ModelComponent."
+ ):
+ component_collection.add_component("NotAComponent")
+
+ def test_remove_component(self, component_collection):
+ # WHEN THEN
+ component_collection.remove_component("TestGaussian1")
+ # EXPECT
+ assert "TestGaussian1" not in component_collection.components
+
+ def test_remove_component_raises(self, component_collection):
+ # WHEN THEN EXPECT
+ with pytest.raises(TypeError, match="Component name must be a string"):
+ component_collection.remove_component(123)
+
+ def test_remove_nonexistent_component_raises(self, component_collection):
+ # WHEN THEN EXPECT
+ with pytest.raises(
+ KeyError, match="No component named 'NonExistentComponent' exists"
+ ):
+ component_collection.remove_component("NonExistentComponent")
+
+ def test_getitem(self, component_collection):
+ # WHEN
+ component = Gaussian(
+ display_name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV"
+ )
+ # THEN
+ component_collection.add_component(component)
+ # EXPECT
+ assert component_collection.components[-1] is component
+
+ def test_list_component_names(self, component_collection):
+ # WHEN THEN
+ components = component_collection.list_component_names()
+ # EXPECT
+ assert len(components) == 2
+ assert components[0] == "TestGaussian1"
+ assert components[1] == "TestLorentzian1"
+
+ def test_clear_components(self, component_collection):
+ # WHEN THEN
+ component_collection.clear_components()
+ # EXPECT
+ assert len(component_collection.components) == 0
+
+ def test_convert_unit(self, component_collection):
+ # WHEN THEN
+ component_collection.convert_unit("eV")
+ # EXPECT
+ for component in component_collection.components:
+ assert component.unit == "eV"
+
+ def test_convert_unit_failure_rolls_back(self, component_collection):
+ # WHEN THEN
+ # Introduce a faulty component that will fail conversion
+ class FaultyComponent(Gaussian):
+ def convert_unit(self, unit: str) -> None:
+ raise RuntimeError("Conversion failed.")
+
+ faulty_component = FaultyComponent(
+ display_name="FaultyComponent", area=1.0, center=0.0, width=1.0, unit="meV"
+ )
+ component_collection.add_component(faulty_component)
+
+ original_units = {
+ component.display_name: component.unit
+ for component in component_collection.components
+ }
+
+ # EXPECT
+ with pytest.raises(RuntimeError, match="Conversion failed."):
+ component_collection.convert_unit("eV")
+
+ # Check that all components have their original units
+ for component in component_collection.components:
+ assert component.unit == original_units[component.display_name]
+
+ def test_set_unit(self, component_collection):
+ # WHEN THEN EXPECT
+ with pytest.raises(
+ AttributeError,
+ match="Unit is read-only. Use convert_unit to change the unit",
+ ):
+ component_collection.unit = "eV"
+
+ def test_evaluate(self, component_collection):
+ # WHEN
+ x = np.linspace(-5, 5, 100)
+ result = component_collection.evaluate(x)
+ # EXPECT
+ expected_result = component_collection.components[0].evaluate(
+ x
+ ) + component_collection.components[1].evaluate(x)
+ np.testing.assert_allclose(result, expected_result, rtol=1e-5)
+
+ def test_evaluate_no_components_raises(self):
+ # WHEN THEN
+ component_collection = ComponentCollection(display_name="EmptyModel")
+ x = np.linspace(-5, 5, 100)
+ # EXPECT
+ with pytest.raises(ValueError, match="No components in the model to evaluate."):
+ component_collection.evaluate(x)
+
+ def test_evaluate_component(self, component_collection):
+ # WHEN THEN
+ x = np.linspace(-5, 5, 100)
+ result1 = component_collection.evaluate_component(x, "TestGaussian1")
+ result2 = component_collection.evaluate_component(x, "TestLorentzian1")
+
+ # EXPECT
+ expected_result1 = component_collection.components[0].evaluate(x)
+ expected_result2 = component_collection.components[1].evaluate(x)
+ np.testing.assert_allclose(result1, expected_result1, rtol=1e-5)
+ np.testing.assert_allclose(result2, expected_result2, rtol=1e-5)
+
+ def test_evaluate_nonexistent_component_raises(self, component_collection):
+ # WHEN
+ x = np.linspace(-5, 5, 100)
+
+ # THEN EXPECT
+ with pytest.raises(
+ KeyError, match="No component named 'NonExistentComponent' exists"
+ ):
+ component_collection.evaluate_component(x, "NonExistentComponent")
+
+ def test_evaluate_component_no_components_raises(self):
+ # WHEN THEN
+ component_collection = ComponentCollection(display_name="EmptyModel")
+ x = np.linspace(-5, 5, 100)
+ # EXPECT
+ with pytest.raises(ValueError, match="No components in the model to evaluate."):
+ component_collection.evaluate_component(x, "AnyComponent")
+
+ def test_evaluate_component_invalid_name_type_raises(self, component_collection):
+ # WHEN
+ x = np.linspace(-5, 5, 100)
+
+ # THEN EXPECT
+ with pytest.raises(
+ TypeError,
+ match="Component unique name must be a string, got instead.",
+ ):
+ component_collection.evaluate_component(x, 123)
+
+ # ───── Utilities ─────
+
+ def test_normalize_area(self, component_collection):
+ # WHEN THEN
+ component_collection.normalize_area()
+ # EXPECT
+ x = np.linspace(-10000, 10000, 1000000) # Lorentzians have long tails
+ result = component_collection.evaluate(x)
+ numerical_area = simpson(result, x)
+ assert np.isclose(numerical_area, 1.0, rtol=1e-4)
+
+ def test_normalize_area_no_components_raises(self):
+ # WHEN THEN
+ component_collection = ComponentCollection(display_name="EmptyModel")
+ # EXPECT
+ with pytest.raises(
+ ValueError, match="No components in the model to normalize."
+ ):
+ component_collection.normalize_area()
+
+ @pytest.mark.parametrize(
+ "area_value",
+ [np.nan, 0.0, np.inf],
+ ids=["NaN area", "Zero area", "Infinite area"],
+ )
+ def test_normalize_area_not_finite_area_raises(
+ self, component_collection, area_value
+ ):
+ # WHEN THEN
+ component_collection.components[0].area = area_value
+ component_collection.components[1].area = area_value
+
+ # EXPECT
+ with pytest.raises(ValueError, match="cannot normalize."):
+ component_collection.normalize_area()
+
+ def test_normalize_area_non_area_component_warns(self, component_collection):
+ # WHEN
+ component1 = Polynomial(
+ display_name="TestPolynomial", coefficients=[1, 2, 3], unit="meV"
+ )
+ component_collection.add_component(component1)
+
+ # THEN EXPECT
+ with pytest.warns(UserWarning, match="does not have an 'area' "):
+ component_collection.normalize_area()
+
+ def test_get_all_parameters(self, component_collection):
+ # WHEN THEN
+ parameters = component_collection.get_all_parameters()
+ # EXPECT
+ assert len(parameters) == 6
+
+ expected_names = {
+ "TestGaussian1 area",
+ "TestGaussian1 center",
+ "TestGaussian1 width",
+ "TestLorentzian1 area",
+ "TestLorentzian1 center",
+ "TestLorentzian1 width",
+ }
+ actual_names = {param.name for param in parameters}
+ assert actual_names == expected_names
+ assert all(isinstance(param, Parameter) for param in parameters)
+
+ def test_get_parameters_no_components(self):
+ component_collection = ComponentCollection(display_name="EmptyModel")
+ # WHEN THEN
+ parameters = component_collection.get_all_parameters()
+ # EXPECT
+ assert len(parameters) == 0
+
+ def test_get_fit_parameters(self, component_collection):
+ # WHEN
+
+ # Fix one parameter and make another dependent
+ component_collection.components[0].area.fixed = True
+ component_collection.components[1].width.make_dependent_on(
+ "comp1_width",
+ {"comp1_width": component_collection.components[0].width},
+ )
+
+ # THEN
+ fit_parameters = component_collection.get_fit_parameters()
+
+ # EXPECT
+ assert len(fit_parameters) == 4
+
+ expected_names = {
+ "TestGaussian1 center",
+ "TestGaussian1 width",
+ "TestLorentzian1 area",
+ "TestLorentzian1 center",
+ }
+ actual_names = {param.name for param in fit_parameters}
+ assert actual_names == expected_names
+ assert all(isinstance(param, Parameter) for param in fit_parameters)
+
+ def test_fix_and_free_all_parameters(self, component_collection):
+ # WHEN THEN
+ component_collection.fix_all_parameters()
+
+ # EXPECT
+ for param in component_collection.get_all_parameters():
+ assert param.fixed is True
+
+ # WHEN
+ component_collection.free_all_parameters()
+
+ # THEN
+ for param in component_collection.get_all_parameters():
+ assert param.fixed is False
+
+ def test_contains(self, component_collection):
+ assert "TestGaussian1" in component_collection
+ assert "TestLorentzian1" in component_collection
+ assert "NonExistentComponent" not in component_collection
+
+ gaussian_component = component_collection.components[0]
+ lorentzian_component = component_collection.components[1]
+ assert gaussian_component in component_collection
+ assert lorentzian_component in component_collection
+
+ # WHEN THEN
+ fake_component = Gaussian(
+ display_name="FakeGaussian", area=1.0, center=0.0, width=1.0, unit="meV"
+ )
+ # EXPECT
+ assert fake_component not in component_collection
+ assert 123 not in component_collection # Invalid type
+
+ def test_repr_contains_name_and_components(self, component_collection):
+ # WHEN THEN
+ rep = repr(component_collection)
+ # EXPECT
+ assert "ComponentCollection" in rep
+ assert "TestGaussian" in rep
+
+ # def test_copy(self, component_collection):
+ # # WHEN THEN
+ # component_collection.temperature = 300
+ # model_copy = copy(component_collection)
+ # # EXPECT
+ # assert model_copy is not component_collection
+ # assert model_copy.display_name == component_collection.display_name
+ # assert len(model_copy.components) == len(component_collection.components)
+ # for comp in component_collection.components:
+ # copied_comp = model_copy.components[
+ # model_copy.list_component_names().index(comp.display_name)
+ # ]
+ # assert copied_comp is not comp
+ # assert copied_comp.display_name == comp.display_name
+ # for param_orig, param_copy in zip(
+ # comp.get_all_parameters(), copied_comp.get_all_parameters()
+ # ):
+ # assert param_copy is not param_orig
+ # assert param_copy.name == param_orig.name
+ # assert param_copy.value == param_orig.value
+ # assert param_copy.fixed == param_orig.fixed
+
+ # def test_to_dict(self, component_collection):
+ # # WHEN THEN
+ # model_dict = component_collection.to_dict()
+ # # EXPECT
+ # assert model_dict["display_name"] == "TestComponentCollection"
+ # assert len(model_dict["components"]) == 2
+ # component_names = [
+ # comp_dict["display_name"] for comp_dict in model_dict["components"]
+ # ]
+ # assert "TestGaussian1" in component_names
+ # assert "TestLorentzian1" in component_names
+ def test_to_dict(self, component_collection):
+ # WHEN THEN
+ model_dict = component_collection.to_dict()
+
+ # EXPECT
+ assert model_dict["display_name"] == component_collection.display_name
+ assert model_dict["unit"] == component_collection.unit
+ assert len(model_dict["components"]) == len(component_collection.components)
+
+ for comp, comp_dict in zip(
+ component_collection.components, model_dict["components"]
+ ):
+ assert comp_dict["@class"] == type(comp).__name__
+ assert comp_dict["display_name"] == comp.display_name
+ assert comp_dict["unit"] == comp.unit
+
+ def test_from_dict(self, component_collection):
+ # WHEN
+ model_dict = component_collection.to_dict()
+
+ # THEN
+ new_model = ComponentCollection.from_dict(model_dict)
+
+ # EXPECT
+ assert new_model.display_name == component_collection.display_name
+ assert len(new_model.components) == len(component_collection.components)
+
+ # Compare each component and its parameters
+ for orig_comp, new_comp in zip(
+ component_collection.components, new_model.components
+ ):
+ assert type(new_comp) is type(orig_comp)
+ assert new_comp.display_name == orig_comp.display_name
+ assert new_comp.unit == orig_comp.unit
+
+ orig_params = orig_comp.get_all_parameters()
+ new_params = new_comp.get_all_parameters()
+ assert len(orig_params) == len(new_params)
+ for param_orig, param_new in zip(orig_params, new_params):
+ assert param_new.name == param_orig.name
+ assert param_new.value == param_orig.value
+ assert param_new.fixed == param_orig.fixed
+
+ def test_copy(self, component_collection):
+ # WHEN
+ component_collection.temperature = 300
+ model_copy = copy(component_collection)
+
+ # THEN: collection-level checks
+ assert model_copy is not component_collection
+ assert model_copy.display_name == component_collection.display_name
+ assert len(model_copy.components) == len(component_collection.components)
+
+ # EXPECT: deep copy, same order
+ for orig_comp, copied_comp in zip(
+ component_collection.components, model_copy.components
+ ):
+ # New object
+ assert copied_comp is not orig_comp
+
+ # Same type and display name
+ assert type(copied_comp) is type(orig_comp)
+ assert copied_comp.display_name == orig_comp.display_name
+ assert copied_comp.unit == orig_comp.unit
+
+ # Parameters are deep-copied and equivalent
+ orig_params = orig_comp.get_all_parameters()
+ copied_params = copied_comp.get_all_parameters()
+
+ assert len(orig_params) == len(copied_params)
+
+ for param_orig, param_copy in zip(orig_params, copied_params):
+ assert param_copy is not param_orig
+ assert param_copy.value == param_orig.value
+ assert param_copy.min == param_orig.min
+ assert param_copy.max == param_orig.max
+ assert param_copy.fixed == param_orig.fixed