diff --git a/examples/component_collection.ipynb b/examples/component_collection.ipynb new file mode 100644 index 0000000..f64254c --- /dev/null +++ b/examples/component_collection.ipynb @@ -0,0 +1,83 @@ +{ + "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": "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/examples/diffusion_model.ipynb b/examples/diffusion_model.ipynb new file mode 100644 index 0000000..f2fb212 --- /dev/null +++ b/examples/diffusion_model.ipynb @@ -0,0 +1,77 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "64deaa41", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from easydynamics.sample_model import BrownianTranslationalDiffusion\n", + "\n", + "import matplotlib.pyplot as plt\n", + "\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "784d9e82", + "metadata": {}, + "outputs": [], + "source": [ + "# Create Brownian Translational Diffusion model and plot the model for different Q values.\n", + "# Q is in Angstrom^-1 and energy in meV.\n", + "\n", + "Q=np.linspace(0.5,2,7)\n", + "energy=np.linspace(-2, 2, 501)\n", + "scale=1.0\n", + "diffusion_coefficient = 2.4e-9 # m^2/s\n", + "diffusion_unit= \"m**2/s\"\n", + "\n", + "diffusion_model=BrownianTranslationalDiffusion(display_name=\"DiffusionModel\", scale=scale, diffusion_coefficient= diffusion_coefficient, diffusion_unit=diffusion_unit)\n", + "\n", + "component_collections=diffusion_model.create_component_collections(Q)\n", + "\n", + "\n", + "cmap = plt.cm.jet\n", + "nQ = len(component_collections)\n", + "plt.figure()\n", + "for Q_index in range(len(component_collections)):\n", + " color = cmap(Q_index / (nQ - 1))\n", + " y=component_collections[Q_index].evaluate(energy)\n", + " plt.plot(energy, y, label=f'Q={Q[Q_index]} Å^-1',color=color)\n", + " \n", + "plt.legend()\n", + "plt.show()\n", + "plt.xlabel('Energy (meV)')\n", + "plt.ylabel('Intensity (arb. units)')\n", + "plt.title('Brownian Translational Diffusion Model') " + ] + } + ], + "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/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py index a64ffd2..8c22392 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, @@ -6,13 +7,16 @@ Polynomial, Voigt, ) +from .diffusion_model import BrownianTranslationalDiffusion, DiffusionModel __all__ = [ - "SampleModel", + "ComponentCollection", "Gaussian", "Lorentzian", "Voigt", "DeltaFunction", "DampedHarmonicOscillator", "Polynomial", + "DiffusionModel", + "BrownianTranslationalDiffusion", ] diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py new file mode 100644 index 0000000..36b6280 --- /dev/null +++ b/src/easydynamics/sample_model/component_collection.py @@ -0,0 +1,296 @@ +import warnings +from typing import List, Optional, Union + +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 + +from .components.model_component import ModelComponent + +Numeric = Union[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, + display_name: str = "MyComponentCollection", + unit: str | sc.Unit = "meV", + components: List[ModelComponent] = [], + ): + """ + Initialize a new ComponentCollection. + + Parameters + ---------- + name : str + Name of the sample model. + unit : str or sc.Unit, optional + Unit of the sample model. Defaults to "meV". + **kwargs : ModelComponent + Initial model components to add to the ComponentCollection. Keys are component names, values are ModelComponent instances. + """ + + super().__init__(display_name=display_name) + + self._unit = unit + self._components = [] + + # Add initial components if provided. Used for serialization. + if components: + 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.display_name}' is already in the collection." + ) + + for comp in self._components: + if comp.display_name == component.display_name: + raise ValueError( + f"A component with the name '{component.display_name}' is already in the collection." + ) + + self._components.append(component) + + def remove_component(self, name: str) -> None: + if not isinstance(name, str): + raise TypeError("Component name must be a string.") + + for comp in self._components: + if comp.display_name == name: + self._components.remove(comp) + return + + raise KeyError(f"No component named '{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.display_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 = 0.0 + + for component in self.components: + if hasattr(component, "area"): + area_params.append(component.area) + total_area += component.area.value + else: + warnings.warn( + f"Component '{component.display_name}' does not have an 'area' attribute and will be skipped in normalization.", + UserWarning, + ) + + if total_area == 0: + raise ValueError("Total area is zero; cannot normalize.") + + if not np.isfinite(total_area): + raise ValueError("Total area is not finite; cannot normalize.") + + for param in area_params: + param.value /= total_area + + 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) -> Optional[Union[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: Union[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: Union[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, + name: str, + ) -> np.ndarray: + """ + Evaluate a single component by name. + + Parameters + ---------- + x : Number, list, np.ndarray, sc.Variable, or sc.DataArray + Energy axis. + name : str + Component 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(name, str): + raise TypeError( + (f"Component name must be a string, got {type(name)} instead.") + ) + + matches = [comp for comp in self.components if comp.display_name == name] + if not matches: + raise KeyError(f"No component named '{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_all_parameters(): + param.fixed = True + + def free_all_parameters(self) -> None: + """ + Free all fixed parameters in the model. + """ + for param in self.get_all_parameters(): + param.fixed = False + + def __contains__(self, item: Union[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 name + return any(comp.display_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.display_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..8099770 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Union import numpy as np import scipp as sc @@ -18,7 +18,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,31 +27,73 @@ 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", + display_name: str = "DampedHarmonicOscillator", + area: Numeric | Parameter = 1.0, + center: Numeric | Parameter = 1.0, + width: Numeric | Parameter = 1.0, + unit: str | sc.Unit = "meV", ): # Validate inputs and create Parameters if not given self.validate_unit(unit) self._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 ) - width = self._create_width_parameter(width=width, name=name, unit=self._unit) + center.min = 0.0 # Enforce center >= 0 for DHO + width = self._create_width_parameter( + width=width, name=display_name, unit=self._unit + ) super().__init__( - name=name, + display_name=display_name, unit=unit, - area=area, - center=center, - width=width, ) + 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] ) -> np.ndarray: @@ -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..b8fb7d2 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Union import numpy as np import scipp as sc @@ -21,7 +21,7 @@ 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. + display_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". @@ -29,30 +29,57 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): 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", + display_name: str = "DeltaFunction", + center: None | Numeric | Parameter = None, + area: Numeric | Parameter = 1.0, + unit: str | sc.Unit = "meV", ): # Validate inputs and create Parameters if not given self.validate_unit(unit) self._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=True, unit=self._unit + center=center, name=display_name, fix_if_none=True, unit=self._unit ) super().__init__( - name=name, + display_name=display_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: + """Set the center parameter value.""" + 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 +115,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(display_name = {self.display_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..2805a39 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,7 +17,7 @@ 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. + display_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. @@ -28,33 +26,71 @@ class Gaussian(CreateParametersMixin, ModelComponent): 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", + display_name: str = "Gaussian", + area: Numeric | Parameter = 1.0, + center: Numeric | Parameter | None = None, + width: Numeric | Parameter = 1.0, + unit: str | sc.Unit = "meV", ): # 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, + ) # 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 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 +104,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(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/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 7551eaf..51aad17 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Union import numpy as np import scipp as sc @@ -19,7 +19,7 @@ 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. + display_name (str): Display 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) @@ -28,33 +28,73 @@ class Lorentzian(CreateParametersMixin, ModelComponent): 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", + display_name: str = "Lorentzian", + area: Numeric | Parameter = 1.0, + center: Numeric | Parameter | None = None, + width: Numeric | Parameter = 1.0, + unit: str | sc.Unit = "meV", ): # Validate inputs and create Parameters if not given self.validate_unit(unit) self._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=True, unit=self._unit + center=center, name=display_name, fix_if_none=True, unit=self._unit + ) + width = self._create_width_parameter( + width=width, name=display_name, unit=self._unit ) - width = self._create_width_parameter(width=width, name=name, unit=self._unit) super().__init__( - name=name, + display_name=display_name, unit=unit, - area=area, - center=center, - width=width, ) + 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 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 +108,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(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/model_component.py b/src/easydynamics/sample_model/components/model_component.py index d6fecf6..caa95dd 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -2,29 +2,28 @@ 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, + display_name: str = None, + unit: str | sc.Unit = "meV", ): self.validate_unit(unit) - super().__init__(name=name, **kwargs) + super().__init__(display_name=display_name) self._unit = unit @property @@ -48,17 +47,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 +117,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 +126,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,7 +142,7 @@ 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. @@ -156,4 +155,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__}(name={self.display_name})" diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index 226c4ea..183d45c 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, Union 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): @@ -18,15 +18,17 @@ class Polynomial(ModelComponent): Polynomial function component. c0 + c1*x + c2*x^2 + ... + cN*x^N Args: + display_name (str): Display name of the Polynomial component. 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. """ def __init__( self, - name: Optional[str] = "Polynomial", - coefficients: Optional[Sequence[Union[Numeric, Parameter]]] = (0.0,), - unit: Union[str, sc.Unit] = "meV", + display_name: str = "Polynomial", + coefficients: Sequence[Union[Numeric, Parameter]] = (0.0,), + unit: str | sc.Unit = "meV", ): self.validate_unit(unit) @@ -49,7 +51,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." @@ -60,16 +62,15 @@ def __init__( 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) + super().__init__(display_name=display_name, unit=unit) @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 self._coefficients - @coefficient_values.setter - def coefficient_values(self, coeffs: Sequence[Union[Numeric, Parameter]]) -> None: + @coefficients.setter + def coefficients(self, coeffs: Sequence[Union[Numeric, Parameter]]) -> None: """Replace the coefficients. Length must match current number of coefficients.""" if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( @@ -90,6 +91,12 @@ def coefficient_values(self, coeffs: Sequence[Union[Numeric, Parameter]]) -> Non "Each coefficient must be either a numeric value or a Parameter." ) + @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 evaluate( self, x: Union[Numeric, list, np.ndarray, sc.Variable, sc.DataArray] ) -> np.ndarray: @@ -105,9 +112,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 name {self.display_name} has negative values, " + "which may not be physically meaningful.", + UserWarning, ) return result @@ -122,7 +129,7 @@ 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: @@ -156,7 +163,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(display_name = {self.display_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..7c62a1b 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Union import numpy as np import scipp as sc @@ -11,7 +11,7 @@ from .model_component import ModelComponent -Numeric = Union[float, int] +Numeric = float | int class Voigt(CreateParametersMixin, ModelComponent): @@ -20,7 +20,7 @@ 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. + display_name (str): Name of the component. 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. @@ -30,44 +30,95 @@ class Voigt(CreateParametersMixin, ModelComponent): 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", + display_name: str = "Voigt", + 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", ): # Validate inputs and create Parameters if not given self.validate_unit(unit) self._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=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, + display_name=display_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: + """Set the center parameter value.""" + 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] ) -> np.ndarray: @@ -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(display_name = {self.display_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/sample_model/diffusion_model/__init__.py b/src/easydynamics/sample_model/diffusion_model/__init__.py new file mode 100644 index 0000000..47a02b5 --- /dev/null +++ b/src/easydynamics/sample_model/diffusion_model/__init__.py @@ -0,0 +1,7 @@ +from .brownian_translational_diffusion import BrownianTranslationalDiffusion +from .diffusion_model_base import DiffusionModel + +__all__ = [ + "DiffusionModel", + "BrownianTranslationalDiffusion", +] diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py new file mode 100644 index 0000000..3b92997 --- /dev/null +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -0,0 +1,298 @@ +from numbers import Number +from typing import Dict, List, Optional, Union + +import numpy as np +import scipp as sc +from easyscience.variable import DescriptorNumber, Parameter +from scipp.constants import hbar as scipp_hbar + +from easydynamics.sample_model.component_collection import ComponentCollection +from easydynamics.sample_model.components import Lorentzian +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModel, +) + +Numeric = Union[float, int] + + +class BrownianTranslationalDiffusion(DiffusionModel): + """ + Model of Brownian translational diffusion, consisting of a Lorentzian + function for each Q-value, where the width is given by :math:`DQ^2`. + Q is assumed to have units of 1/angstrom. + Creates ComponentCollections with Lorentzian components for given Q-values. + + Example usage: + Q=np.linspace(0.5,2,7) + energy=np.linspace(-2, 2, 501) + scale=1.0 + diffusion_coefficient = 2.4e-9 # m^2/s + diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel", scale=scale, diffusion_coefficient= diffusion_coefficient) + component_collections=diffusion_model.create_component_collections(Q) + See also the examples. + """ + + def __init__( + self, + display_name: Optional[str] = "BrownianTranslationalDiffusion", + unit: Optional[Union[str, sc.Unit]] = "meV", + scale: Optional[Union[Parameter, Numeric]] = 1.0, + diffusion_coefficient: Optional[Union[Parameter, Numeric]] = 1.0, + diffusion_unit: Optional[str] = "m**2/s", + ): + """ + Initialize a new BrownianTranslationalDiffusion model. + + Parameters + ---------- + display_name : str + Display name of the diffusion model. + unit : str or sc.Unit, optional + Energy unit for the underlying Lorentzian components. Defaults to "meV". + scale : float or Parameter, optional + Scale factor for the diffusion model. + diffusion_coefficient : float or Parameter, optional + Diffusion coefficient D. If a number is provided, it is assumed to be in the unit given by diffusion_unit. Defaults to 1.0. + diffusion_unit : str, optional + Unit for the diffusion coefficient D. Default is "meV*Å**2". Options are 'meV*Å**2' or 'm**2/s' + + """ + if not isinstance(scale, (Parameter, Numeric)): + raise TypeError("scale must be a Parameter or a number.") + + if not isinstance(diffusion_coefficient, (Parameter, Numeric)): + raise TypeError("diffusion_coefficient must be a Parameter or a number.") + + if not isinstance(diffusion_unit, str): + raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.") + + if diffusion_unit == "meV*Å**2" or diffusion_unit == "meV*angstrom**2": + # In this case, hbar is absorbed in the unit of D + self._hbar = DescriptorNumber("hbar", 1.0) + elif diffusion_unit == "m**2/s" or diffusion_unit == "m^2/s": + self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) + else: + raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.") + + if not isinstance(scale, Parameter): + scale = Parameter(name="scale", value=float(scale), fixed=False, min=0.0) + + if not isinstance(diffusion_coefficient, Parameter): + diffusion_coefficient = Parameter( + name="diffusion_coefficient", + value=float(diffusion_coefficient), + fixed=False, + unit=diffusion_unit, + ) + super().__init__( + display_name=display_name, + unit=unit, + ) + self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") + self._scale = scale + self._diffusion_coefficient = diffusion_coefficient + + @property + def scale(self) -> Parameter: + """ + Get the scale parameter of the diffusion model. + + Returns + ------- + Parameter + Scale parameter. + """ + return self._scale + + @scale.setter + def scale(self, scale: Numeric) -> None: + if not isinstance(scale, (Numeric)): + raise TypeError("scale must be a number.") + self._scale.value = scale + + @property + def diffusion_coefficient(self) -> Parameter: + """ + Get the diffusion coefficient parameter D. + + Returns + ------- + Parameter + Diffusion coefficient D. + """ + return self._diffusion_coefficient + + @diffusion_coefficient.setter + def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: + if not isinstance(diffusion_coefficient, (Numeric)): + raise TypeError("diffusion_coefficient must be a number.") + self._diffusion_coefficient.value = diffusion_coefficient + + def calculate_width(self, Q: np.ndarray) -> np.ndarray: + """ + Calculate the half-width at half-maximum (HWHM) for the diffusion model. + + Parameters + ---------- + Q : np.ndarray + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + HWHM values in the unit of the model (e.g., meV). + """ + + if isinstance(Q, Numeric): + Q = np.array([Q]) + + if isinstance(Q, list): + Q = np.array(Q) + + if not isinstance(Q, np.ndarray): + raise TypeError("Q must be a numpy array.") + + width_list = [] + for Q_value in Q: + # Q is given as a float, so we need to divide by angstrom**2 to get the right units + width = ( + self._hbar + * self.diffusion_coefficient + * Q_value**2 + / (self._angstrom**2) + ) + width.convert_unit(self.unit) + width_list.append(width.value) + width = np.array(width_list) + + return width + + def calculate_EISF(self, Q: np.ndarray) -> np.ndarray: + """ + Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational diffusion model. + + Parameters + ---------- + Q : np.ndarray + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + EISF values (dimensionless). + """ + if not isinstance(Q, np.ndarray): + raise TypeError("Q must be a numpy array.") + EISF = np.zeros_like(Q) + return EISF + + def calculate_QISF(self, Q: np.ndarray) -> np.ndarray: + """ + Calculate the Quasi-Elastic Incoherent Structure Factor (QISF). + + Parameters + ---------- + Q : np.ndarray + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + QISF values (dimensionless). + """ + + if not isinstance(Q, np.ndarray): + raise TypeError("Q must be a numpy array.") + QISF = np.ones_like(Q) + return QISF + + def create_component_collections( + self, + Q: Union[Number, list, np.ndarray], + component_name: str = "Lorentzian", + ) -> List[ComponentCollection]: + """ + Create ComponentCollection components for the Brownian translational diffusion model at given Q values. + Args: + ---------- + Q : Number, list, or np.ndarray + Scattering vector values. + component_name : str + Name of the Lorentzian component. + width_name : str + Name of the width parameter. + Returns + ------- + List[ComponentCollection] + List of ComponentCollections with Lorentzian components. + """ + + if isinstance(Q, Numeric): + Q = np.array([Q]) + + if isinstance(Q, list): + Q = np.array(Q) + + if not isinstance(Q, np.ndarray): + raise TypeError("Q must be a number, list, or numpy array.") + + if Q.ndim > 1: + raise ValueError("Q must be a 1-dimensional array.") + + if not isinstance(component_name, str): + raise TypeError("component_name must be a string.") + + component_collection_list = [None] * len(Q) + # In more complex models, this is used to scale the area of the Lorentzians and the delta function. + QISF = self.calculate_QISF(Q) + + # Create a Lorentzian component for each Q-value, with width D*Q^2 and area equal to scale. No delta function, as the EISF is 0. + for i in range(len(Q)): + component_collection_list[i] = ComponentCollection( + display_name=f"{self.display_name}_Q{Q[i]:.2f}", unit=self.unit + ) + + lorentzian_component = Lorentzian( + display_name=component_name, area=self.scale * QISF[i], unit=self.unit + ) + + # Make the width dependent on Q + dependency_expression = self._write_width_dependency_expression(Q[i]) + dependency_map = self._write_width_dependency_map_expression() + + lorentzian_component.width.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map=dependency_map, + ) + + # Resolving the dependency can do weird things to the units, so we make sure it's correct. + lorentzian_component.width.convert_unit(self.unit) + component_collection_list[i].add_component(lorentzian_component) + + return component_collection_list + + def _write_width_dependency_expression(self, Q: float) -> str: + """ + Write the dependency expression for the width as a function of Q to make dependent Parameters. + """ + if not isinstance(Q, (float)): + raise TypeError("Q must be a float.") + + # Q is given as a float, so we need to add the units + return f"hbar * D* {Q} **2*1/(angstrom**2)" + + def _write_width_dependency_map_expression(self) -> Dict[str, str]: + """ + Write the dependency map expression to make dependent Parameters. + """ + return { + "D": self.diffusion_coefficient, + "hbar": self._hbar, + "angstrom": self._angstrom, + } + + def __repr__(self): + """ + String representation of the BrownianTranslationalDiffusion model. + """ + return f"BrownianTranslationalDiffusion(display_name={self.display_name}, diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})" diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py new file mode 100644 index 0000000..876fd55 --- /dev/null +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -0,0 +1,50 @@ +import scipp as sc +from easyscience.base_classes.model_base import ModelBase + + +class DiffusionModel(ModelBase): + """ + Base class for constructing diffusion models. + """ + + def __init__( + self, + display_name="MyDiffusionModel", + unit: str | sc.Unit = "meV", + ): + """ + Initialize a new DiffusionModel. + + Parameters + ---------- + display_name : str + Display name of the diffusion model. + unit : str or sc.Unit, optional + Unit of the diffusion model. Defaults to "meV". + """ + + if not (unit is None or isinstance(unit, (str, sc.Unit))): + raise TypeError("unit must be None, a string, or a scipp Unit") + + super().__init__(display_name=display_name) + self._unit = unit + + @property + def unit(self) -> str | sc.Unit: + """ + Get the unit of the DiffusionModel. + + 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 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..c14ad38 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 diff --git a/tests/unit_tests/sample_model/components/test_gaussian.py b/tests/unit_tests/sample_model/components/test_gaussian.py index faffb31..e705590 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 "display_name = TestGaussian" 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..f42b11b 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 "display_name = TestLorentzian" 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..9213cc5 100644 --- a/tests/unit_tests/sample_model/components/test_model_component.py +++ b/tests/unit_tests/sample_model/components/test_model_component.py @@ -1,3 +1,5 @@ +from typing import Union + import numpy as np import pytest import scipp as sc @@ -5,16 +7,18 @@ from easydynamics.sample_model.components.model_component import ModelComponent +Numeric = Union[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 +48,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..3ee6fb4 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 @@ -47,7 +47,7 @@ def test_initialization(self, polynomial: Polynomial): ) def test_input_type_validation_raises(self, kwargs, expected_message): with pytest.raises(TypeError, match=expected_message): - Polynomial(name="TestPolynomial", **kwargs) + Polynomial(display_name="TestPolynomial", **kwargs) @pytest.mark.parametrize("invalid_coeffs", [[], None]) def test_no_coefficients_raises(self, invalid_coeffs): @@ -55,12 +55,13 @@ def test_no_coefficients_raises(self, invalid_coeffs): with pytest.raises( ValueError, match="At least one coefficient must be provided" ): - Polynomial(name="TestPolynomial", coefficients=invalid_coeffs) + Polynomial(display_name="TestPolynomial", coefficients=invalid_coeffs) 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 +91,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 +107,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 +122,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 +165,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 diff --git a/tests/unit_tests/sample_model/components/test_voigt.py b/tests/unit_tests/sample_model/components/test_voigt.py index 9b59b9d..842adec 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 diff --git a/tests/unit_tests/sample_model/diffusion_model/test_brownian_translational_diffusion.py b/tests/unit_tests/sample_model/diffusion_model/test_brownian_translational_diffusion.py new file mode 100644 index 0000000..908a2ed --- /dev/null +++ b/tests/unit_tests/sample_model/diffusion_model/test_brownian_translational_diffusion.py @@ -0,0 +1,292 @@ +import numpy as np +import pytest +import scipp as sc +from easyscience.variable import DescriptorNumber, Parameter +from scipp.constants import hbar as scipp_hbar + +from easydynamics.sample_model.diffusion_model.brownian_translational_diffusion import ( + BrownianTranslationalDiffusion, +) + +hbar_1 = DescriptorNumber("hbar", 1.0) +hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) +angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") + + +class TestBrownianTranslationalDiffusion: + @pytest.fixture + def brownian_diffusion_model(self): + return BrownianTranslationalDiffusion() + + def test_init_default(self, brownian_diffusion_model): + # WHEN THEN EXPECT + assert brownian_diffusion_model.display_name == "BrownianTranslationalDiffusion" + assert brownian_diffusion_model.unit == "meV" + assert brownian_diffusion_model.scale.value == 1.0 + assert brownian_diffusion_model.diffusion_coefficient.value == 1.0 + + @pytest.mark.parametrize( + "kwargs, expected_message", + [ + ( + { + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": 1.0, + "diffusion_unit": "m**2/s", + }, + "unit must be None, a string, or a scipp Unit", + ), + ( + { + "unit": 123, + "scale": "invalid", + "diffusion_coefficient": 1.0, + "diffusion_unit": "m**2/s", + }, + "scale must be a Parameter or a number.", + ), + ( + { + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": "invalid", + "diffusion_unit": "m**2/s", + }, + "diffusion_coefficient must be a Parameter or a number.", + ), + ( + { + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": 1.0, + "diffusion_unit": 123, + }, + "diffusion_unit must be ", + ), + ], + ) + def test_input_type_validation_raises(self, kwargs, expected_message): + with pytest.raises(TypeError, match=expected_message): + BrownianTranslationalDiffusion( + display_name="BrownianTranslationalDiffusion", **kwargs + ) + + def test_diffusion_unit_value_error(self): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match="diffusion_unit must be ."): + BrownianTranslationalDiffusion( + display_name="BrownianTranslationalDiffusion", + unit="meV", + scale=1.0, + diffusion_coefficient=1.0, + diffusion_unit="invalid_unit", + ) + + def test_init_with_parameters(self): + # WHEN + + scale = Parameter(name="scale_param", value=2.0) + diffusion_coefficient = Parameter( + name="diffusion_coefficient", value=3.0, unit="m**2/s" + ) + + # THEN + brownian_diffusion_model = BrownianTranslationalDiffusion( + display_name="CustomBrownianDiffusion", + unit="meV", + scale=scale, + diffusion_coefficient=diffusion_coefficient, + ) + + # EXPECT + assert brownian_diffusion_model.display_name == "CustomBrownianDiffusion" + assert brownian_diffusion_model.unit == "meV" + assert brownian_diffusion_model.scale is scale + assert brownian_diffusion_model.diffusion_coefficient is diffusion_coefficient + + def test_scale_setter(self, brownian_diffusion_model): + # WHEN + brownian_diffusion_model.scale = 2.0 + + # THEN EXPECT + assert brownian_diffusion_model.scale.value == 2.0 + + def test_scale_setter_raises(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="scale must be a number."): + brownian_diffusion_model.scale = "invalid" # Invalid type + + def test_diffusion_coefficient_setter(self, brownian_diffusion_model): + # WHEN + brownian_diffusion_model.diffusion_coefficient = 3.0 + + # THEN EXPECT + assert brownian_diffusion_model.diffusion_coefficient.value == 3.0 + + def test_diffusion_coefficient_setter_raises(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="diffusion_coefficient must be a number."): + brownian_diffusion_model.diffusion_coefficient = "invalid" # Invalid type + + def test_calculate_width_type_error(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="Q must be a numpy array."): + brownian_diffusion_model.calculate_width(Q="invalid") # Invalid type + + def test_calculate_width(self, brownian_diffusion_model): + # WHEN + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # WHEN + widths = brownian_diffusion_model.calculate_width(Q_values) + + # THEN EXPECT + unit_conversion_factor = sc.to_unit( + 1 + * sc.Unit(brownian_diffusion_model.diffusion_coefficient.unit) + * scipp_hbar + / (1 * sc.Unit("Å") ** 2), + "meV", + ) + expected_widths = 1.0 * unit_conversion_factor.value * (Q_values**2) + np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) + + def test_calculate_width_diffusion_unit_mev_angstrom2(self): + # WHEN + diffusion_model = BrownianTranslationalDiffusion( + diffusion_coefficient=2.0, diffusion_unit="meV*Å**2" + ) + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # WHEN + widths = diffusion_model.calculate_width(Q_values) + + # THEN EXPECT + expected_widths = 2.0 * (Q_values**2) + np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) + + def test_calculate_EISF(self, brownian_diffusion_model): + # WHEN + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # THEN + EISF = brownian_diffusion_model.calculate_EISF(Q_values) + + # EXPECT + expected_EISHF = np.zeros_like(Q_values) + np.testing.assert_array_equal(EISF, expected_EISHF) + + def test_calculate_EISF_type_error(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="Q must be a numpy array."): + brownian_diffusion_model.calculate_EISF(Q="invalid") # Invalid type + + def test_calculate_QISF(self, brownian_diffusion_model): + # WHEN + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # THEN + QISF = brownian_diffusion_model.calculate_QISF(Q_values) + + # EXPECT + expected_QISF = np.ones_like(Q_values) + np.testing.assert_array_equal(QISF, expected_QISF) + + def test_calculate_QISF_type_error(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="Q must be a numpy array."): + brownian_diffusion_model.calculate_QISF(Q="invalid") # Invalid type + + @pytest.mark.parametrize( + "Q", + [ + (0.5), + ([1.0, 2.0, 3.0]), + (np.array([1.0, 2.0, 3.0])), + ], + ids=[ + "python_scalar", + "python_list", + "numpy_array", + ], + ) + def test_create_component_collections(self, brownian_diffusion_model, Q): + # WHEN + + # THEN + component_collections = brownian_diffusion_model.create_component_collections( + Q=Q + ) + + # EXPECT + expected_widths = brownian_diffusion_model.calculate_width(Q) + for model_index in range(len(component_collections)): + model = component_collections[model_index] + assert len(model.components) == 1 + component = model.components[0] + assert component.display_name == "Lorentzian" + assert component.width.unit == brownian_diffusion_model.unit + assert component.width.value == expected_widths[model_index] + assert component.width.independent is False + + def test_create_component_collections_component_name_must_be_string( + self, brownian_diffusion_model + ): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="component_name must be a string."): + brownian_diffusion_model.create_component_collections( + Q=np.array([0.1, 0.2, 0.3]), component_name=123 + ) + + def test_create_component_collections_Q_type_error(self, brownian_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match="Q must be a "): + brownian_diffusion_model.create_component_collections( + Q="invalid" + ) # Invalid type + + def test_create_component_collections_Q_1dimensional_error( + self, brownian_diffusion_model + ): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match="Q must be a 1-dimensional array."): + brownian_diffusion_model.create_component_collections( + Q=np.array([[0.1, 0.2], [0.3, 0.4]]) + ) # Invalid shape + + def test_write_width_dependency_expression(self, brownian_diffusion_model): + # WHEN THEN + expression = brownian_diffusion_model._write_width_dependency_expression(0.5) + + # EXPECT + expected_expression = "hbar * D* 0.5 **2*1/(angstrom**2)" + assert expression == expected_expression + + def test_write_width_dependency_map_expression(self, brownian_diffusion_model): + # WHEN THEN + expression_map = ( + brownian_diffusion_model._write_width_dependency_map_expression() + ) + + # EXPECT + expected_map = { + "D": brownian_diffusion_model.diffusion_coefficient, + "hbar": brownian_diffusion_model._hbar, + "angstrom": brownian_diffusion_model._angstrom, + } + + assert expression_map == expected_map + + def test_write_width_dependency_expression_raises(self, brownian_diffusion_model): + with pytest.raises(TypeError, match="Q must be a float"): + brownian_diffusion_model._write_width_dependency_expression("invalid") + + def test_repr(self, brownian_diffusion_model): + # WHEN THEN + repr_str = repr(brownian_diffusion_model) + + # EXPECT + assert "BrownianTranslationalDiffusion" in repr_str + assert "diffusion_coefficient" in repr_str + assert "scale=" in repr_str diff --git a/tests/unit_tests/sample_model/diffusion_model/test_diffusion_model.py b/tests/unit_tests/sample_model/diffusion_model/test_diffusion_model.py new file mode 100644 index 0000000..5653b35 --- /dev/null +++ b/tests/unit_tests/sample_model/diffusion_model/test_diffusion_model.py @@ -0,0 +1,24 @@ +import pytest + +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModel, +) + + +class TestDiffusionModel: + @pytest.fixture + def diffusion_model(self): + return DiffusionModel(display_name="TestDiffusionModel", unit="meV") + + def test_init_default(self, diffusion_model): + # WHEN THEN EXPECT + assert diffusion_model.display_name == "TestDiffusionModel" + assert diffusion_model.unit == "meV" + + def test_unit_setter_raises(self, diffusion_model): + # WHEN THEN EXPECT + with pytest.raises( + AttributeError, + match="Unit is read-only. Use convert_unit to change the unit between allowed types", + ): + diffusion_model.unit = "eV" 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..b7b2054 --- /dev/null +++ b/tests/unit_tests/sample_model/test_component_collection.py @@ -0,0 +1,372 @@ +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" + ) + component2 = Lorentzian( + display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, unit="meV" + ) + 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 == [] + + # ───── 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_duplicate_component_name_raises(self, component_collection): + # WHEN THEN + component = Gaussian( + display_name="TestGaussian1", area=1.0, center=0.0, width=1.0, unit="meV" + ) + # EXPECT + with pytest.raises(ValueError, match="is already in the collection"): + component_collection.add_component(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 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