diff --git a/docs/dust.rst b/docs/dust.rst index 7fac519..9d293d4 100644 --- a/docs/dust.rst +++ b/docs/dust.rst @@ -7,6 +7,15 @@ dust .. autofunction:: suggest_opacity_sampling +.. autoclass:: IsotropicDust + :show-inheritance: + +.. autoclass:: HenyeyGreensteinDust + :show-inheritance: + +.. autoclass:: GeneralDust + :show-inheritance: + .. autoclass:: Dust :show-inheritance: diff --git a/docs/dustcreation.rst b/docs/dustcreation.rst index e214cbb..18ee0c1 100644 --- a/docs/dustcreation.rst +++ b/docs/dustcreation.rst @@ -2,23 +2,28 @@ Creating a dust model ===================== Before running a radiative transfer simulation, you need to create a dust model that defines the optical properties of -the dust grains in your simulation. Pinball-rt provides a :class:`~pinballrt.dust.Dust` class that allows you to create and manipulate dust -models. To set up a dust model, the absorption and scattering opacities as a function of wavelength and grain size -distribution parameters (maximum dust grain size, size distribution power-law index, and sub-species relative abundances) are needed. At present, these -must be obtained from external sources and provided to pinball-rt. Here we'll use simple power-law prescription, but in -practice you would typically use opacities derived from laboratory measurements or Mie theory calculations. Note that -the opacities should include astropy units to ensure that there is no ambiguity. +the dust grains in your simulation. Pinball-rt provides a :class:`~pinballrt.dust.Dust` class that allows you to create +and manipulate dust models. In practice, there are three more specific dust models available: +:class:`~pinballrt.dust.IsotropicDust`, :class:`~pinballrt.dust.HenyeyGreensteinDust`, and +:class:`~pinballrt.dust.GeneralDust` that inherit from :class:`~pinballrt.dust.Dust` and enable more specific control +over dust scattering properties. To set up a dust model, the absorption and scattering opacities as a function of wavelength +and grain size distribution parameters (maximum dust grain size, size distribution power-law index, and sub-species +relative abundances) are needed. At present, these must be obtained from external sources and provided to pinball-rt. +Here we'll use simple power-law prescription, but in practice you would typically use opacities derived from laboratory +measurements or Mie theory calculations. Note that the opacities should include astropy units to ensure that there is no +ambiguity. To enable maximum flexibility, opacities do not need to be provided on a regular grid. Instead, the opacities should be provided at some number (nsamples) of points in the N-dimensional parameter space defined by relevant inputs for determining the opacity (maximum grain size, size distribution power-law index, sub-species abundances), and for each sample they -should be provided at a specified set of wavelengths. The Dust class will then use machine learning to learn the opacities -at any point in the parameter space during the simulation. A helper-function, :func:`~pinballrt.dust.suggest_opacity_sampling` is provided to -help provide efficient sampling of the parameter space, but the user is free to provide any set of samples they choose. +should be provided at a specified set of wavelengths. The :class:`~pinballrt.dust.Dust` class will then use machine learning +to learn the opacities at any point in the parameter space during the simulation. A helper-function, +:func:`~pinballrt.dust.suggest_opacity_sampling` is provided to help provide efficient sampling of the parameter space, +but the user is free to provide any set of samples they choose. .. code-block:: python - from pinballrt.dust import Dust, suggest_opacity_sampling + from pinballrt.dust import IsotropicDust, suggest_opacity_sampling import numpy as np import astropy.units as u @@ -42,27 +47,25 @@ help provide efficient sampling of the parameter space, but the user is free to kappa_abs = 1.0 * (wavelengths.to(u.micron)/100.0)**power_law_index * u.cm**2 / u.g kappa_scat = 0.5 * (wavelengths.to(u.micron)/100.0)**power_law_index * u.cm**2 / u.g - # Create the Dust object. - dust = Dust(lam=wavelengths[0,:], - amax=amax[:,0], - p=p[:,0], - kabs=kappa_abs, - ksca=kappa_scat) + # Create the IsotropicDust object. + dust = IsotropicDust(lam=wavelengths[0,:], + amax=amax[:,0], + p=p[:,0], + kabs=kappa_abs, + ksca=kappa_scat) -This creates a Dust object with the specified opacities, however a few additional steps are needed before the dust model can be used in a -radiative transfer simulation. The Dust object uses a machine learning model to produce opacity values during the simulation, as well as to +This creates an :class:`~pinballrt.dust.IsotropicDust` object with the specified opacities, however a few additional steps are needed before the dust model can be used in a +radiative transfer simulation. The :class:`~pinballrt.dust.IsotropicDust` object uses a machine learning model to produce opacity values during the simulation, as well as to randomnly sample photon frequencies emitted by dust grains during the simulation, but these models need to be trained first. To set up the training, we use the :meth:`~pinballrt.dust.Dust.learn` method. For example, to set up the model to learn the absorption opacity: .. code-block:: python # Set up the training parameters. - dust.learn( - model="kabs", - test_fraction=0.1, - val_fraction=0.1, - hidden_units=(48, 48, 48), - ) + dust.learn(model="kabs", + test_fraction=0.1, + val_fraction=0.1, + hidden_units=(48, 48, 48)) This example sets up a simple neural network model with three hidden layers of 48 units each to learn the dust absorption opacity. The training will use the kabs samples provided above, which had 100 samples across dust properties at 100 wavelengths for 10,000 total samples, with 10% of @@ -86,13 +89,13 @@ a simulation. In short: .. code-block:: python for model in ["ksca", "pmo", "random_nu"]: - if model in ["kabs", "ksca"]: - d.learn(model=model, hidden_units=(16,)*6, overwrite=True) - else: - d.learn(model=model, hidden_units=(48,)*3, overwrite=True) + if model in ["kabs", "ksca"]: + d.learn(model=model, hidden_units=(16,)*6, overwrite=True) + else: + d.learn(model=model, hidden_units=(48,)*3, overwrite=True) - d.fit(epochs=300, batch_size=1000) - d.test_model(plot=True) + d.fit(epochs=300, batch_size=1000) + d.test_model(plot=True) This will further create models to produce the scattering opacity, planck mean opacity, and random frequencies sampled from the dust emission spectrum. Having to train the dust model before every simulation would be inefficient, so once the model is trained it can be saved to a file using the :meth:`~pinballrt.dust.Dust.save` method, @@ -117,6 +120,52 @@ pinball-rt provides a pre-trained dust model that can be used directly without n # Load the pre-trained dust model. dust = load("yso.dst") +Creating a Henyey-Greenstein dust model follows the same process as above, but with the addition of needing to train a model to produce the scattering asymmetry parameter (g) as a function of wavelength and dust properties: + +.. code-block:: python + + from pinballrt.dust import HenyeyGreensteinDust + + g = np.tanh(p - np.log10(wavelengths.to(u.micron).value)) + + # Create the Henyey-Greenstein dust model. + dust = HenyeyGreensteinDust(lam=wavelengths[0,:], + amax=amax[:,0], + p=p[:,0], + kabs=kappa_abs, + ksca=kappa_scat, + g=g) + + d.learn(model="g", hidden_units=(16,)*6, overwrite=True) + + d.fit(epochs=300, batch_size=1000) + d.test_model(plot=True) + +Similarly, the most general dust model, :class:`~pinballrt.dust.GeneralDust`, follows the same process but with the addition of needing to train a model to produce the scattering phase function as a function of wavelength, scattering angle and dust properties, and additionally to randomly sample scattering angles during the simulation: + +.. code-block:: python + + from pinballrt.dust import GeneralDust + + g = np.repeat(np.expand_dims(np.tanh(p - np.log10(wavelengths.to(u.micron).value)), axis=-1), 5, axis=-1) + theta = np.tile(np.expand_dims(np.linspace(0, 180., 5), axis=(0,1)), (10 if len(dims) > 0 else 1, 10, 1)) * u.deg + scattering_phase_function = (1 - g**2) / (4 * np.pi * (1 + g**2 - 2*g*np.cos(theta.to(u.rad).value))**(3/2)) + + # Create the General dust model. + dust = GeneralDust(lam=wavelengths[0,:], + amax=amax[:,0], + p=p[:,0], + kabs=kappa_abs, + ksca=kappa_scat, + scattering_phase_function=scattering_phase_function + theta=theta[0,0,:]) + + for model in ["scattering_phase_function", "random_direction"]: + d.learn(model=model, hidden_units=(16,)*6, overwrite=True) + + d.fit(epochs=300, batch_size=1000) + d.test_model(plot=True) + Learning to step through high optical depth regions --------------------------------------------------- diff --git a/examples/dust-demo-diana.ipynb b/examples/dust-demo-diana.ipynb index abbcfc4..ef3bb76 100644 --- a/examples/dust-demo-diana.ipynb +++ b/examples/dust-demo-diana.ipynb @@ -10,7 +10,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/users/psheehan/Documents/pinball-warp/pinballrt/dust.py:89: SyntaxWarning: \"is not\" with 'tuple' literal. Did you mean \"!=\"?\n", + "/users/psheehan/Documents/pinball-warp/pinballrt/dust.py:107: SyntaxWarning: \"is not\" with 'tuple' literal. Did you mean \"!=\"?\n", " if hasattr(self, dim) and getattr(self, dim) is not None and getattr(self, dim) is not ():\n" ] } @@ -20,6 +20,7 @@ "from astropy.modeling import models\n", "import astropy.units as u\n", "import numpy as np\n", + "import torch\n", "\n", "import subprocess\n", "\n", @@ -103,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 12, "id": "96a02ab2", "metadata": {}, "outputs": [], @@ -147,74 +148,203 @@ " abundances=(Vcarbon,),\n", " fiducial_values={\"amax\": 3*u.mm, \n", " \"p\": 3.5, \n", - " \"abundances\": (0.15,)})\n", + " \"abundances\": (0.15,)})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "adf95f57", + "metadata": {}, + "outputs": [], + "source": [ + "for model in [\"kabs\", \"ksca\", \"pmo\", \"random_nu\"]:\n", + " print(\"*******************************\")\n", + " print(model)\n", + " print(\"*******************************\")\n", "\n", - "\"\"\"d = HenyeyGreensteinDust(lam=lam, \n", - " kabs=kabs, \n", - " ksca=ksca,\n", - " g=g,\n", - " amax=amax, \n", - " p=p,\n", - " fiducial_values={\"amax\": 3*u.mm, \n", - " \"p\": 3.5, \n", - " \"abundances\": (0.15,)})\"\"\"\n", + " if model in [\"random_direction\"]:\n", + " hidden_units = (48,)*6\n", + " elif model in [\"random_nu\"]:\n", + " hidden_units = (48,)*6\n", + " batch_size = 1000000\n", + " else:\n", + " hidden_units = (32,)*6\n", + " batch_size = 100000\n", + " \n", + " d.learn(model=model, nsamples=1000, hidden_units=hidden_units, overwrite=True)\n", "\n", - "\"\"\"d = GeneralDust(lam=lam,\n", - " kabs=kabs.reshape(-1, lam.size),\n", - " ksca=ksca.reshape(-1, lam.size),\n", - " scattering_phase_function=scattering_phase.reshape(-1, lam.size, theta.size),\n", - " theta=theta.to(u.radian),\n", - " amax=amax,\n", - " p=p,\n", - " default_fiducial_values={\"amax\": 3*u.mm, \n", - " \"p\": 3.5, \n", - " \"abundances\": (0.15,)})\"\"\"" + " d.fit(epochs=5000, batch_size=batch_size, num_workers=50)\n", + " d.test_model(plot=True)" ] }, { "cell_type": "code", - "execution_count": 108, - "id": "adf95f57", + "execution_count": 130, + "id": "def42c2d-9a96-4dca-ba31-580f85c93ebd", "metadata": {}, "outputs": [ { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "Trainer will use only 1 of 2 GPUs because it is running inside an interactive / notebook environment. You may try to set `Trainer(devices=2)` but please note that multi-GPU inside interactive / notebook environments is considered experimental and unstable. Your mileage may vary.\n" + "p: 4.398709161558942, amax: 0.031783722019888265, T: 877.9492371803229, abundances: [np.float64(0.7058875657301152)]\n" ] }, + { + "data": { + "image/png": "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x8df8JqADCyxseYgKqfT6fW40+n0nDtbt27d2LdvH5GRkdjOarbyWQuLiIiI+JxhGJw6dYpu3bpd9NqADCwdO3YkPj6e4uJir8eLiopISUk553q73d7g4yIiIhLYLtayUi9g1/YdO3YsWVlZnmPDMNi8eTPjx4+3sCoRERGxQsAGloyMDFasWMHevXsB+Nvf/obD4eDOO++0uDIRERFpaQHZJQQwfPhwXn31VaZOnUqrVq2w2+2sXLnynEXj/O39999n8eLFVFdX43Q6qays5Ne//jW33XbbBZ/X2FV6w1lz3tvHHnuM9957j3bt2nke69ChA++8804LVBy8nn/+eWbNmsWnn37KmDFjznvd+vXrmTt3LtHR0TidThYsWMB1113XcoUGoca8t6+++irz58+nS5cuXo9/9NFHREVFtUCVwaM5/8b1eds4TX1vA+7z1pALuvnmm43XXnvNc/zBBx8YNpvN2LZt23mfs3HjRqNt27bG7t27DcMwjNdee83o3r27UVZW5vd6g0lz3tvf/e53xqefftoC1YWOgoICo2fPngZwwfcuPz/fiIuLM9atW2cYhmGsWbPGiIuLM/Lz81uo0uDT2Pf2lVdeMV555ZUWqyuYNfXfuD5vG6+p722gfd4GbJdQoHjqqae4/fbbPcdjxozBMAxyc3PP+5yGVumtra3l1Vdf9Xe5QaU576003axZs3jooYcuet0zzzzDwIEDPS0q119/Pf379+fZZ5/1d4lBq7HvrfiPPm/DhwLLRQwbNoyICLPnrKamhj/+8Y8MHDjwgoN/V69eTVpamuf47FV65YzmvLfSNMuWLSMyMpKbb775otd++/cWID09Xb+359GU91b8R5+34UOBpZHuueceEhISWLVqFStXrqRNmzYNXnehVXrz8vJaotSg09j3tt5f//pXxowZw6hRo7jzzjvZt29fC1UaXCoqKvjtb3/LwoULG3V9bm6ufm8bqanvLcDy5csZO3Ys1157LbfeeitbtmzxY4XBrbH/xvV523RN/fwMpM9bBZZG+tOf/sTRo0c9/+EKCwsbvK6pq/RK499bgJ49ezJkyBBWrVrFZ599Ru/evRk2bBgFBQUtWHFweOSRR5gxYwZduzbuBoqVlZX6vW2kpr63iYmJ9O3blw8//JD169fz3e9+lxEjRrB161b/FhqEmvJvXJ+3TdPUz8+A+7y1ehBNsHG5XEaPHj2MuXPnNnj+6NGjBmC8/vrrXo/ffffdxhVXXNESJQati723DamtrTUSExONhx56yI+VBZ+srCxj5MiRhsvlMgzDMPLy8i46MLRNmzbG73//e6/HnnjiCaNt27b+LDXoNOe9bUhaWppx++23+6HC0HKhf+P6vL00Tf38tPrzNmCnNQeK6upqr2mHdrudfv36sXNnw/eZaeoqveGsqe9tQxwOB8nJyeoW+pYVK1ZQVVXF2LFjATh9+jQA999/P+3atePll1+mT58+Xs9JSUnR720jNOe9bchll12m39tGuNC/cX3eXpqmfn5a/XmrLqGLGDp06DmPFRYWXvC+B1qlt3Ga897Onj37nMcOHz5Mz549fVpbsHvkkUfYvHkza9asYc2aNbz55psALFq0iDVr1jT4B3XcuHFev7cAmZmZ+r39lua8t7/5zW/O6aIoKCjQ720DmvpvXJ+3jdfU9zbgPm8tadcJIjabzVi+fLnn+PXXXzfsdrvx2WefeR4bNWqUVxPZxo0bjbi4OGPPnj2e52hdgHM1571NTk423n//fc/x//zP/xgxMTFGTk5OyxQdpBrqtrjtttuMadOmeY7r12FZv369YRiGsW7dOq3D0giNeW+vv/5649lnn/Ucf/TRR4bdbjc++eSTliw1KFzs37g+b5uvqe9toH3eqkvoIp555hmeeuop5s2bh9vtxmaz8cEHH3Dttdd6rqmsrPS6s3SgrNIb6Jrz3j711FMsWrSI//7v/6a6upro6GhWrVrF5ZdfbsWPEBTuv/9+vvzyS8/+5Zdfzptvvsnp06ex2880svbq1Yvly5fzq1/9iqioKJxOJ8uXL6dXr15WlR7wGvveZmRk8Nxzz/HWW29hGAZut5v33nuPG264warSA9bF/o3r87b5mvreBtrnrc0wDMOS7ywiIiLSSBrDIiIiIgFPgUVEREQCngKLiIiIBDwFFhEREQl4CiwiIiIS8BRYREREJOApsIiIiEjAU2ARERGRgKfAIiIiIgFPgUVEREQCngKLiIiIBDwFFhEREQl4/x+67x1yXNTelAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "d.plot_random_nu_model()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2c739068", + "metadata": {}, + "outputs": [], + "source": [ + "d.save(\"diana.iso.dst\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7c510026-d957-4a8a-a637-f3c6a3792236", + "metadata": {}, + "outputs": [], + "source": [ + "state_dict = d.state_dict()\n", + "\n", + "state_dict[\"dust_properties\"][\"g\"] = g\n", + "torch.save(state_dict, \"diana.hg.dst\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ca9f631a-ece0-4d63-85a3-0d86401e5afd", + "metadata": {}, + "outputs": [], + "source": [ + "d = load(\"diana.hg.dst\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6d1d630a-8f0d-4b48-a229-b85c0a98de1c", + "metadata": {}, + "outputs": [], + "source": [ + "hidden_units = (32,)*6\n", + "batch_size = 100000\n", + " \n", + "d.learn(model=\"g\", hidden_units=hidden_units, overwrite=True)\n", + "\n", + "d.fit(epochs=5000, batch_size=batch_size, num_workers=50)\n", + "d.test_model(plot=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2049ed8c-24c2-4e4f-b6db-f8057bbf0253", + "metadata": {}, + "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "*******************************\n", - "random_nu\n", - "*******************************\n" + "p: 3.9263747457427454, log10_amax: -1.4872009276741207, abundances: [np.float64(0.632842498846851)], \n" ] }, + { + "data": { + "image/png": "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FvH66697KpaIiIj4EI/c8K28VMQbvomIiFR05/L7Ww8lFBEREa+loiIiIiJeS0VFREREvJaKioVcLhdjx46tdM8z8iR9p56h77Xs6Tste/pOy543fKeaTGshX8/vjfSdeoa+17Kn77Ts6Tste576TjWZVkRERCoEFRURERHxWh674Vt5+OOsVWZmpsVJzs8fuX01vzfSd+oZ+l7Lnr7TsqfvtOx56jv94/POZvaJT89R2bdvH9HR0VbHEBERkfOQnJxM3bp1z7iNTxcVt9vNgQMHCAkJwTAMq+OIiIjIWTBNk6ysLKKiorDZzjwLxaeLioiIiFRsmkwrIiIiXktFRURERLyWioqX2bFjB127dqV3795WR/FJM2bMIDY2lh49etCrVy82bdpkdSSfl5+fz+jRo/Hz8yMpKcnqOD5v2rRpDBgwgL59+xIbG8sNN9yg7/UCfP/991x++eX07duX7t2706FDB6ZOnWp1rApl/PjxGIbBokWLLNm/iooXmTx5MrfeeutfTiyS01u5ciUjRoxgypQpLFmyhDvvvJO4uDiysrKsjuazkpKS6NWrFykpKRQVFVkdp0IYPnw4jz76KAsXLmTFihUEBQVx2WWX6bbv5+nf//43N954IwsXLmTp0qX84x//4Oabb2b9+vVWR6sQDhw4wKuvvmppBv1G9CLh4eEsXryYxo0bWx3FJ40bN46BAwfSpEkToPgXQmFhIRMmTLA2mA87fvw4kydP5vbbb7c6SoVx9dVXExcXB4DNZuPBBx9k27ZtJCQkWJzMN7344ovcdNNNJcu9e/fGNE127dplYaqK44EHHuCpp56yNIOKihe54oorCAgIsDqGz1q4cCGdOnUqWbbZbHTs2JEFCxZYmMq3tW7dWsW5jE2fPr3UcmBgIICOqJynjh074udXfO/SgoICXnvtNVq2bEm/fv0sTub7Zs6cib+/f0mxtoqKilQI6enpZGZmEhERUWo8MjKS3bt3W5RK5K8tX76cqKgounXrZnUUn3b//fdTs2ZNFixYwLx586hatarVkXxadnY2Y8aM4c0337Q6ioqKVAw5OTkAOByOUuMOh6NknYi3cblcvPrqq4wfPx5/f3+r4/i09957j8OHD9O7d2+6detGSkqK1ZF82jPPPMPIkSOpXbu21VFUVDxt9OjRGIZxxtfWrVutjunzgoODgVMPn7tcrpJ1It7m3nvvZejQoQwePNjqKBWCn58fzz//PG63mzfeeMPqOD4rISGBFStWMHLkSKujAD7+UEJf8NRTTzFq1KgzbhMZGVlOaSqu8PBwnE4naWlppcZTU1OJiYmxKJXInxs9ejTBwcE8//zzVkfxafn5+aXm9tlsNpo2bcrmzZstTOXbZs2aRW5uLn369AEgLy8PgIcffpiwsDA+/vjjcp27pqLiYaGhoYSGhlodo1Lo06cP8fHxJcumaZKQkMCYMWMsTCVyqnHjxpGcnMzkyZMBSv7cduzY0cpYPqlDhw5s3Lix1FhKSorm/FyAZ555hmeeeaZkOSkpiYYNG/LWW29Zco8vnfqRCmP06NHMmjWLxMREAL744gvsdjsjRoywOJnIf33wwQd8/vnnPPDAAyQkJLB69WpmzpzJhg0brI7mkzZv3sysWbNKlj///HO2bdumv/cViB5K6EV++OEH3njjDbZu3UpeXh7t27fnlltu4c4777Q6ms+YMWMGL774IkFBQdhsNt5//31atWpldSyflZ+fz4ABAzh27Bjr1q3j4osvJjo6+pRLbOXsZGVlERYWhtvtPmXdZ599xm233Vb+oXzcu+++y9SpU7HZbLjdbgzD4KmnnmLgwIFWR6sQHn74YX7//XdWrFhBu3btaN68OV9++WW5ZlBREREREa+lUz8iIiLitVRURERExGupqIiIiIjXUlERERERr6WiIiIiIl5LRUVERES8loqKiIiIeC0VFREREfFaKioiIiLitVRURERExGupqIiIiIjXUlERERERr/X/QTCRru7OQv8AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "d.plot_opacity_model(model=\"g\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "30abb72b-0b08-4010-90d3-c76407e42bc5", + "metadata": {}, + "outputs": [], + "source": [ + "d.save(\"diana.hg.dst\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "650d7d24-7fff-403b-b90b-887497521d53", + "metadata": {}, + "outputs": [], + "source": [ + "d = load(\"diana.iso.dst\")\n", + "\n", + "state_dict = d.state_dict()\n", + "\n", + "state_dict[\"dust_properties\"][\"scattering_phase_function\"] = scattering_phase\n", + "state_dict[\"dust_properties\"][\"theta\"] = theta\n", + "torch.save(state_dict, \"diana.gen.dst\", pickle_protocol=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7b497432-9945-4516-81c3-0ed8de1de9b5", + "metadata": {}, + "outputs": [], + "source": [ + "d = load(\"diana.gen.dst\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f77b7548-c86d-473d-9bbe-293ab69f815f", + "metadata": {}, + "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ + "Trainer will use only 1 of 2 GPUs because it is running inside an interactive / notebook environment. You may try to set `Trainer(devices=2)` but please note that multi-GPU inside interactive / notebook environments is considered experimental and unstable. Your mileage may vary.\n", "GPU available: True (cuda), used: True\n", "TPU available: False, using: 0 TPU cores\n", "HPU available: False, using: 0 HPUs\n", - "/lustre/cv/users/psheehan/.conda/envs/pinball-rt/lib/python3.13/site-packages/astropy/units/quantity.py:653: RuntimeWarning: overflow encountered in expm1\n", - " result = super().__array_ufunc__(function, method, *arrays, **kwargs)\n", - "/lustre/cv/users/psheehan/.conda/envs/pinball-rt/lib/python3.13/site-packages/astropy/units/quantity.py:653: RuntimeWarning: overflow encountered in multiply\n", - " result = super().__array_ufunc__(function, method, *arrays, **kwargs)\n", - "/lustre/cv/users/psheehan/.conda/envs/pinball-rt/lib/python3.13/site-packages/pytorch_lightning/callbacks/model_checkpoint.py:751: Checkpoint directory /users/psheehan/Documents/pinball-warp/examples/random_nu_lightning_logs exists and is not empty.\n", + "/lustre/cv/users/psheehan/.conda/envs/pinball-rt/lib/python3.13/site-packages/pytorch_lightning/trainer/connectors/logger_connector/logger_connector.py:76: Starting from v1.9.0, `tensorboardX` has been removed as a dependency of the `pytorch_lightning` package, due to potential conflicts with other packages in the ML ecosystem. For this reason, `logger=True` will use `CSVLogger` as the default logger, unless the `tensorboard` or `tensorboardX` packages are found. Please `pip install lightning[extra]` or one of them to enable TensorBoard support by default\n", + "You are using a CUDA device ('NVIDIA RTX A4000') that has Tensor Cores. To properly utilize them, you should set `torch.set_float32_matmul_precision('medium' | 'high')` which will trade-off precision for performance. For more details, read https://pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html#torch.set_float32_matmul_precision\n", "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0,1]\n", "\n", " | Name | Type | Params | Mode \n", "-------------------------------------------------------\n", - "0 | model | MultiLayerPerceptron | 12.1 K | train\n", + "0 | model | MultiLayerPerceptron | 5.5 K | train\n", "-------------------------------------------------------\n", - "12.1 K Trainable params\n", + "5.5 K Trainable params\n", "0 Non-trainable params\n", - "12.1 K Total params\n", - "0.048 Total estimated model params size (MB)\n", + "5.5 K Total params\n", + "0.022 Total estimated model params size (MB)\n", "16 Modules in train mode\n", "0 Modules in eval mode\n" ] @@ -222,7 +352,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d20fb68c6d8746c2a689b469018d5ba1", + "model_id": "6906deada5bf4284b73d36888a917a4e", "version_major": 2, "version_minor": 0 }, @@ -236,7 +366,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "badc086e2e6e43a5905646fbaff326ed", + "model_id": "4330a05dd7ee4348b0d01f0ad63b268d", "version_major": 2, "version_minor": 0 }, @@ -250,133 +380,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "609b93fd5eea49a5a4b9220c59bd8b51", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "404b8ae6e49f49f9957cf18890360b76", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "63d5ad2542d142f08458a1c7491f9efa", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9567ee2d3bc346d298b12733e53e3171", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2df22fd2e5c34d8ea2ba33fc78cbebf1", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "114660a6b0a94aa3a71af88332210426", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6d7997c707ca40739f5724885155d04b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "78d8d90705234d4281037caca9c519c8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "da569fdbdc614ec592859cfd0b8fb9f1", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0a1ba497c801435188c38c05db7cd5b5", + "model_id": "aab81ac1685649b4830062ca79b990ce", "version_major": 2, "version_minor": 0 }, @@ -390,7 +394,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cfae760efed34a97976c4f896a31369d", + "model_id": "46e1718962fb4d4b8eee37503bfc4f6f", "version_major": 2, "version_minor": 0 }, @@ -404,7 +408,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d0b0da2721fd419da882c09283f96e62", + "model_id": "6926af470cfa40f287ff12cf93401eb4", "version_major": 2, "version_minor": 0 }, @@ -418,7 +422,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ab61da9623c94f2d9aaba426702d0f6f", + "model_id": "3d8630ee002a410e9f8412909dbb3b95", "version_major": 2, "version_minor": 0 }, @@ -432,7 +436,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2996168b6dc9477aa60d834ae77c8839", + "model_id": "6be41a30d5394dc88c179105e9ad884a", "version_major": 2, "version_minor": 0 }, @@ -446,7 +450,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e508e11542b448dea04e98ea2e85da7d", + "model_id": "e0ca11bd3a424065aafaff0b6daaaec8", "version_major": 2, "version_minor": 0 }, @@ -460,7 +464,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "87d0301b47a7498395827f97233e7fa6", + "model_id": "1d61bc526c8e4c8eb13a40d8640b1de5", "version_major": 2, "version_minor": 0 }, @@ -474,7 +478,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e8f2adfbd33f4aaba81a28ff0457d42e", + "model_id": "abbe1b0be2de4a06b068c2b94e76f8aa", "version_major": 2, "version_minor": 0 }, @@ -488,7 +492,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2639cc8c47d74915a2326df004f5341f", + "model_id": "8d4ab35bbd6745b582d08f1c9a527a4e", "version_major": 2, "version_minor": 0 }, @@ -502,7 +506,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a7fe1e0d8c4e4e66b0ae3aa67870e40c", + "model_id": "50dd2f83484840368943e706612c2853", "version_major": 2, "version_minor": 0 }, @@ -516,7 +520,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ca8fc70a9bec4831bca93c8d6ceda92f", + "model_id": "93346e703eea426dac596f9531918e82", "version_major": 2, "version_minor": 0 }, @@ -530,7 +534,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "53401163fa314102831916fb9cb1b84a", + "model_id": "81779ae69d684eacada21cb7251114ac", "version_major": 2, "version_minor": 0 }, @@ -544,7 +548,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "73e8194c26394f11af1002a12af323d5", + "model_id": "94d6a6da8108434fa39ff7ac13338363", "version_major": 2, "version_minor": 0 }, @@ -558,7 +562,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "39e26565ece7478b8f4a4ab11392fdda", + "model_id": "fad10dc5f98d4798ad734bf826743656", "version_major": 2, "version_minor": 0 }, @@ -572,7 +576,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a18ac1ad01314dc8980e9491625ddda3", + "model_id": "48688b73e8c24fee9cfa667ae34effc8", "version_major": 2, "version_minor": 0 }, @@ -586,7 +590,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fddb551746d04b50af16f6c21b8098ec", + "model_id": "595471c970f94042acdc02110479cd4d", "version_major": 2, "version_minor": 0 }, @@ -600,7 +604,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4806f25bbc9349a2bc66cccfd9fcb5c4", + "model_id": "1f87e8f1584140508e2eb479c1832005", "version_major": 2, "version_minor": 0 }, @@ -614,7 +618,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6c49471dea4645479c586f4015fa9519", + "model_id": "826ad5029ab04d09806facd8a476fc27", "version_major": 2, "version_minor": 0 }, @@ -628,7 +632,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c6b610a1fdec47de9f59f416c6425c8f", + "model_id": "fc1a7fdf2ce64393abb05d26e4f8a3ac", "version_major": 2, "version_minor": 0 }, @@ -642,7 +646,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2f23cc03329c421cba98b60b5e3b93a4", + "model_id": "f029aeaa83ab43a3b26fdcd0c35126c4", "version_major": 2, "version_minor": 0 }, @@ -656,7 +660,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c0839686a9c34cd78d5c6a43b8d59db5", + "model_id": "440c656389a34fdf92e5bb6aa0c23993", "version_major": 2, "version_minor": 0 }, @@ -670,7 +674,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "476e1ebd6d04489193b8aa21a4e37819", + "model_id": "b2dc5ec5e22c4ed0bf0dc12be0ba6faa", "version_major": 2, "version_minor": 0 }, @@ -684,7 +688,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f3bf3da853904b2aa536ecfd06170300", + "model_id": "7571aee9394b48ec84c7498e5904370d", "version_major": 2, "version_minor": 0 }, @@ -698,7 +702,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "11f28b42a6a44140984b68aad06638e2", + "model_id": "985c3f6568fd4bd9b5043331ca7d7e06", "version_major": 2, "version_minor": 0 }, @@ -712,7 +716,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3ff9b7bc8f024beca7d63fa42cbfd841", + "model_id": "1978ff6bbe4b4663a3972529dceebda6", "version_major": 2, "version_minor": 0 }, @@ -726,7 +730,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a26a91f730344525be88654dcb5ec73c", + "model_id": "ed322c70a26c420194a7b9ee9541f14b", "version_major": 2, "version_minor": 0 }, @@ -740,7 +744,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "034e9b98391f48159986260e93467b3a", + "model_id": "0da3d7f1bd2a4a3d92bead510224fb13", "version_major": 2, "version_minor": 0 }, @@ -754,7 +758,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "770fe398bdc2472a8b0d7ce39496301d", + "model_id": "f34de18c830d4c5a8b4e7e46508d794b", "version_major": 2, "version_minor": 0 }, @@ -768,7 +772,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2ecc38eee39144f6b737fe52a20d1b7f", + "model_id": "549d99fdc7d5413bb7a0ec8a6a8014af", "version_major": 2, "version_minor": 0 }, @@ -782,7 +786,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e7d06065a6804fc196b2f98d010c0998", + "model_id": "ae8c37ff72634ef8a29592d017549c95", "version_major": 2, "version_minor": 0 }, @@ -796,7 +800,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cb4d978ada6a45e387faaa020aa01302", + "model_id": "1199d26de93743a9abb2fc6217c1889e", "version_major": 2, "version_minor": 0 }, @@ -810,7 +814,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f12c4f7f31d441cc9af081d30ad9eabd", + "model_id": "648c7533e063460394bb8076ec2391a9", "version_major": 2, "version_minor": 0 }, @@ -824,7 +828,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "963852dcd4174e5ca350593a7d8919d4", + "model_id": "00608da5c8294169bd2f171a8547ec11", "version_major": 2, "version_minor": 0 }, @@ -838,7 +842,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9dd5c106324349299ff07fc6f4e9d0f9", + "model_id": "2fa439140b0d471cbd8d235ae6147a66", "version_major": 2, "version_minor": 0 }, @@ -852,7 +856,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9a6700986e49488a8f73a47b0fbcde6d", + "model_id": "ffbf4bd2e79244a888b72afce3e8e271", "version_major": 2, "version_minor": 0 }, @@ -866,7 +870,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "805519fd4f9349cd93d5dd3b8320e64d", + "model_id": "8377c41fb3994b3bb52b1e757e6b8630", "version_major": 2, "version_minor": 0 }, @@ -880,7 +884,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "105e01369a724cbf91372404d032fb36", + "model_id": "6a4d5b1cbf154f4ebc39f205a7b7efb3", "version_major": 2, "version_minor": 0 }, @@ -894,7 +898,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3b905576f00b464eb69112ed0b8aeb40", + "model_id": "dacd06f043754e758e311be6d50d4bd6", "version_major": 2, "version_minor": 0 }, @@ -908,7 +912,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1c5265d4b9a74b8f9c0b362d2073a170", + "model_id": "c271197f7d054d6db70cc4f661b97e72", "version_major": 2, "version_minor": 0 }, @@ -922,7 +926,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "207846babc6e4f50a79907e9ad783764", + "model_id": "174b9a53c6c74feb886a4f01f3c2e581", "version_major": 2, "version_minor": 0 }, @@ -936,7 +940,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2d635b74657a416981b4ff9e5133bf74", + "model_id": "6e2b5701448646caa2ba8633751fb2cd", "version_major": 2, "version_minor": 0 }, @@ -950,7 +954,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ad57119dcb204038be43b1f9886464e4", + "model_id": "b8ace5f7ef424e6588b11ff152c00ccb", "version_major": 2, "version_minor": 0 }, @@ -964,7 +968,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7dd9a8fa970d49a1a4b57bc4645dbfe2", + "model_id": "d0d6959e2e9040638e2bfb7c59bb8ed9", "version_major": 2, "version_minor": 0 }, @@ -978,7 +982,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e5f7c902bba947d7a517bc97f3b03143", + "model_id": "1b198d385a914693b4af2c820b210417", "version_major": 2, "version_minor": 0 }, @@ -992,7 +996,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e98b2b1996924cbaa5a300d1fa73aac4", + "model_id": "7c03559e306f41c284a5a20740d709d2", "version_major": 2, "version_minor": 0 }, @@ -1006,7 +1010,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4f6e2883df2d4ac0897e081ccce587d6", + "model_id": "4dbc080767a640ebbd4de5f30bc09176", "version_major": 2, "version_minor": 0 }, @@ -1020,7 +1024,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "1acc023cbc244b10b459ff2484e4a5a2", + "model_id": "c889dd7e2f454948841b1589cdefc5a1", "version_major": 2, "version_minor": 0 }, @@ -1034,7 +1038,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "fe1272c1c54e4a40a232fb3ab9a81d9b", + "model_id": "e57163d8926c47c091f5a9b3474dfced", "version_major": 2, "version_minor": 0 }, @@ -1048,7 +1052,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "178ef5718f184d018cfa2b859320fea9", + "model_id": "469bef6848044da3b5d76f5539b6640e", "version_major": 2, "version_minor": 0 }, @@ -1062,7 +1066,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5d2a398959434b459dd878ee025d4b26", + "model_id": "54df823210624510a9f80016d763193d", "version_major": 2, "version_minor": 0 }, @@ -1076,7 +1080,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c19295868f474d37b8120583634cdefc", + "model_id": "fbcffd09aaaf4a72812d1bd0b177c85a", "version_major": 2, "version_minor": 0 }, @@ -1090,7 +1094,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a6aa448473ee4e00ab6f14d7346a9c3e", + "model_id": "e981268c657041ef833c9260c1374b2e", "version_major": 2, "version_minor": 0 }, @@ -1104,7 +1108,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3a3b92e42e9d4d6080a1ff343391354e", + "model_id": "f4a931e6db674f1b8eaa5e97416c24c8", "version_major": 2, "version_minor": 0 }, @@ -1118,7 +1122,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e2dd4f26d32041b797533059bc8a97db", + "model_id": "1b41a20eb6db48f9a7678e6bd4ecf4fe", "version_major": 2, "version_minor": 0 }, @@ -1128,1205 +1132,22 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aee23782cf1f41a69b2252c47e87dbf5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a349accedde44b758f62ff9cba83c683", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f2d9daf90dae42699321d52f847e1483", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1e1b217f794d42388b388259c79cba0f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7cc0e7f77ab04e2e800f59dd5780e6c0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3de54b3326eb417dad74cbb3455eaa72", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c492f07fae55410d8bc785ae52abc691", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "682e778eef9a4f4089fd190fd6a195bc", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "befc6e00646b4107bd42ee1ee7e9cdff", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9245d66c88244b0aa44d239dd49ce56e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d92367b7dcc841b89a243a85a2371d90", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c34d29be72f048479b5272ec74685d1b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3e863f7dbfa54e578c0c8b64a0ef5916", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "713f4916b5f443d59a51ff15756cc8ab", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1ec59c34b94e48289e9d7a6ca5c392e1", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "34071280bc97426287c9d09ce744b7a4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "844a850d949842afb0ae11eac6c86926", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "72c6b56179584fe58918a95955b896d2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8f5ce7eda2fa4a1992fda02fb3415447", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b63393baa06b4363a5c4e2863c1bc974", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "db38ba0c890a4cbfa6a0cda596d33ee1", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ce6629bdf3ce42f78a5f80a8bb6a6556", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "012302d393e1404e96ffa77176ebbfc5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c1bb27a8166d44f394c28077f15f4cc3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3fb1ed005ccf4802802750abf5d36211", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cf89df735d54485bb242cfead2ceb54e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b6eaf53bffd5450c990f3497f9a1fab3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "eaf23cdb3e954e65b4382ac6154155e5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fdb23be4653141b6bc4e208dfd8ef302", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "89760c2895a04c77ae3ded99b4cb7e3b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "81c4478ba636490d9153caa78e012d29", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "956bca5fa2ec4bfdad8b4bf8d9da317e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "686e118b731040deb321dcfd2a956708", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a110c358a2be436e9e379c3c836e4e7a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b586c8b055954f3b8825f97317f4bc9f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bdea48c5eda04a89bd6df7963d187a8c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e6eeb3a0b3224e6f8b2c009b775f19aa", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2f9cd5e6c3e4461998b53936f96d39b3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d764a8ae1943468b810fee540863ecf8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9e16a9fc577c4cab8db2bd98a3d7ce1f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "78b958b47f5d41e28a6c12c647768446", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "33cfe158345f45a089e2eb43dff4ed59", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "59accdd64c3943cdb48a5dab93c1214c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7e69564e7a224674aa70c9a7d668053f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3a9db7dbddd74361bc736ae2e4a66030", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d3b7679a319e4a97b6ac8928c4bb4b99", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "86ddfe2291884d24b1d7474d6f1c0f1f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "da13e8353b044b0586a8b084a1484f66", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0cff7cf08ca9488287080191a1e2041d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8b5f3fd87e244ee9a442676093aefe20", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "73f79d1531fc45e2a49f619941b466b3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b6f872203b394d968886a99172153ccb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "23301c4975ab4294a5eb6a2a00ea5c45", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "670e501ce25f45fab53eb3681f987231", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "de8886c824b34f3981518e5650ca8fd9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2ef7d8634f794e1898cb6402285f71e6", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "03086e88ec0b4927b65a3c96dd3ec2ff", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "efa60fb592324416900eab52a6d3a835", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "defa82962a2847d2aa43811812b1d841", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3ee5a9f6c0bd490095125d6a27d08d19", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "97a3b51949304dab9e2ca8919acf0ce7", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "07ea40583bde42158a3fb39c964f7b56", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b13f6dcc1c2c443b83446309e2bb8338", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8744763371f14effb3d85d08e66aa397", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "54a952be2f5a42b0b8170f8dfa122a41", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "becd3460344545e18dea3c26c9275647", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "45a5e9666c6c40b5aaf939261635f8e4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b802d9770f9646dbaf7bbe2fe320f01a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ff8048c984764d2e82cc630ee2d001d9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c26ac6a4701048a9a7ea8fb28c596f0c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bbd7037207e74c18a9544eeb0b492d3f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "116c3950dae44790943b8f70f80d3d0f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b3b308024e0d459a93c4ba433b836200", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "df81aedc8fa44d69877bcdc223d2df3b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ea0db2e9396f4c368da1c4c5f7c6a6d3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0615befa5bf74bd4ba119ec64c25aa6c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ee03c35c5ced4d9485da86cd7989c535", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5c9765aac46c4501b12621ec418e201d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "458f14439353485b8d6a4cac553e0301", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Validation: | …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "\n", - "Detected KeyboardInterrupt, attempting graceful shutdown ...\n" - ] - }, - { - "ename": "SystemExit", - "evalue": "1", - "output_type": "error", - "traceback": [ - "An exception has occurred, use %tb to see the full traceback.\n", - "\u001b[31mSystemExit\u001b[39m\u001b[31m:\u001b[39m 1\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/lustre/cv/users/psheehan/.conda/envs/pinball-rt/lib/python3.13/site-packages/IPython/core/interactiveshell.py:3756: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.\n", - " warn(\"To exit: use 'exit', 'quit', or Ctrl-D.\", stacklevel=1)\n" - ] } ], "source": [ - "for model in [\"kabs\", \"ksca\", \"pmo\", \"random_nu\"]:\n", - "#for model in [\"kabs\", \"ksca\", \"pmo\", \"scattering_phase_function\", \"random_nu\", \"random_direction\"]:\n", - "#for model in [\"scattering_phase_function\"]:\n", - "#for model in [\"random_direction\"]:\n", - " print(\"*******************************\")\n", - " print(model)\n", - " print(\"*******************************\")\n", - "\n", - " if model in [\"random_direction\"]:\n", - " hidden_units = (48,)*6\n", - " elif model in [\"random_nu\"]:\n", - " hidden_units = (48,)*6\n", - " batch_size = 10000000\n", - " else:\n", - " hidden_units = (32,)*6\n", - " batch_size = 100000\n", + "hidden_units = (32,)*6\n", + "batch_size = 1000000\n", " \n", - " d.learn(model=model, nsamples=1000, hidden_units=hidden_units, overwrite=True)\n", + "d.learn(model=\"scattering_phase_function\", hidden_units=hidden_units, overwrite=True, checkpoint=False)\n", "\n", - " d.fit(epochs=5000, batch_size=100000, num_workers=50)\n", - " d.test_model(plot=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 130, - "id": "def42c2d-9a96-4dca-ba31-580f85c93ebd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "p: 4.398709161558942, amax: 0.031783722019888265, T: 877.9492371803229, abundances: [np.float64(0.7058875657301152)]\n" - ] - }, - { - "data": { - "image/png": "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x8df8JqADCyxseYgKqfT6fW40+n0nDtbt27d2LdvH5GRkdjOarbyWQuLiIiI+JxhGJw6dYpu3bpd9NqADCwdO3YkPj6e4uJir8eLiopISUk553q73d7g4yIiIhLYLtayUi9g1/YdO3YsWVlZnmPDMNi8eTPjx4+3sCoRERGxQsAGloyMDFasWMHevXsB+Nvf/obD4eDOO++0uDIRERFpaQHZJQQwfPhwXn31VaZOnUqrVq2w2+2sXLnynEXj/O39999n8eLFVFdX43Q6qays5Ne//jW33XbbBZ/X2FV6w1lz3tvHHnuM9957j3bt2nke69ChA++8804LVBy8nn/+eWbNmsWnn37KmDFjznvd+vXrmTt3LtHR0TidThYsWMB1113XcoUGoca8t6+++irz58+nS5cuXo9/9NFHREVFtUCVwaM5/8b1eds4TX1vA+7z1pALuvnmm43XXnvNc/zBBx8YNpvN2LZt23mfs3HjRqNt27bG7t27DcMwjNdee83o3r27UVZW5vd6g0lz3tvf/e53xqefftoC1YWOgoICo2fPngZwwfcuPz/fiIuLM9atW2cYhmGsWbPGiIuLM/Lz81uo0uDT2Pf2lVdeMV555ZUWqyuYNfXfuD5vG6+p722gfd4GbJdQoHjqqae4/fbbPcdjxozBMAxyc3PP+5yGVumtra3l1Vdf9Xe5QaU576003axZs3jooYcuet0zzzzDwIEDPS0q119/Pf379+fZZ5/1d4lBq7HvrfiPPm/DhwLLRQwbNoyICLPnrKamhj/+8Y8MHDjwgoN/V69eTVpamuf47FV65YzmvLfSNMuWLSMyMpKbb775otd++/cWID09Xb+359GU91b8R5+34UOBpZHuueceEhISWLVqFStXrqRNmzYNXnehVXrz8vJaotSg09j3tt5f//pXxowZw6hRo7jzzjvZt29fC1UaXCoqKvjtb3/LwoULG3V9bm6ufm8bqanvLcDy5csZO3Ys1157LbfeeitbtmzxY4XBrbH/xvV523RN/fwMpM9bBZZG+tOf/sTRo0c9/+EKCwsbvK6pq/RK499bgJ49ezJkyBBWrVrFZ599Ru/evRk2bBgFBQUtWHFweOSRR5gxYwZduzbuBoqVlZX6vW2kpr63iYmJ9O3blw8//JD169fz3e9+lxEjRrB161b/FhqEmvJvXJ+3TdPUz8+A+7y1ehBNsHG5XEaPHj2MuXPnNnj+6NGjBmC8/vrrXo/ffffdxhVXXNESJQati723DamtrTUSExONhx56yI+VBZ+srCxj5MiRhsvlMgzDMPLy8i46MLRNmzbG73//e6/HnnjiCaNt27b+LDXoNOe9bUhaWppx++23+6HC0HKhf+P6vL00Tf38tPrzNmCnNQeK6upqr2mHdrudfv36sXNnw/eZaeoqveGsqe9tQxwOB8nJyeoW+pYVK1ZQVVXF2LFjATh9+jQA999/P+3atePll1+mT58+Xs9JSUnR720jNOe9bchll12m39tGuNC/cX3eXpqmfn5a/XmrLqGLGDp06DmPFRYWXvC+B1qlt3Ga897Onj37nMcOHz5Mz549fVpbsHvkkUfYvHkza9asYc2aNbz55psALFq0iDVr1jT4B3XcuHFev7cAmZmZ+r39lua8t7/5zW/O6aIoKCjQ720DmvpvXJ+3jdfU9zbgPm8tadcJIjabzVi+fLnn+PXXXzfsdrvx2WefeR4bNWqUVxPZxo0bjbi4OGPPnj2e52hdgHM1571NTk423n//fc/x//zP/xgxMTFGTk5OyxQdpBrqtrjtttuMadOmeY7r12FZv369YRiGsW7dOq3D0giNeW+vv/5649lnn/Ucf/TRR4bdbjc++eSTliw1KFzs37g+b5uvqe9toH3eqkvoIp555hmeeuop5s2bh9vtxmaz8cEHH3Dttdd6rqmsrPS6s3SgrNIb6Jrz3j711FMsWrSI//7v/6a6upro6GhWrVrF5ZdfbsWPEBTuv/9+vvzyS8/+5Zdfzptvvsnp06ex2880svbq1Yvly5fzq1/9iqioKJxOJ8uXL6dXr15WlR7wGvveZmRk8Nxzz/HWW29hGAZut5v33nuPG264warSA9bF/o3r87b5mvreBtrnrc0wDMOS7ywiIiLSSBrDIiIiIgFPgUVEREQCngKLiIiIBDwFFhEREQl4CiwiIiIS8BRYREREJOApsIiIiEjAU2ARERGRgKfAIiIiIgFPgUVEREQCngKLiIiIBDwFFhEREQl4/x+67x1yXNTelAAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "d.plot_random_nu_model()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2c739068", - "metadata": {}, - "outputs": [], - "source": [ - "d.save(\"diana.iso.dst\")" + "d.fit(epochs=100, batch_size=batch_size, num_workers=10)\n", + "d.test_model(plot=True)" ] }, { "cell_type": "code", "execution_count": null, - "id": "7c510026-d957-4a8a-a637-f3c6a3792236", + "id": "01830713-30e7-4ddc-adad-92c06584ee3b", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/dust-demo.ipynb b/examples/dust-demo.ipynb index d1efb80..23a7c35 100644 --- a/examples/dust-demo.ipynb +++ b/examples/dust-demo.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "461d2258", "metadata": {}, "outputs": [ @@ -16,7 +16,7 @@ } ], "source": [ - "from pinballrt.dust import Dust, load, suggest_opacity_sampling\n", + "from pinballrt.dust import IsotropicDust, load, suggest_opacity_sampling\n", "import astropy.units as u\n", "import numpy as np" ] @@ -61,12 +61,12 @@ "kappa_scat = 0.5 * (wavelengths.to(u.micron).value/100.0)**power_law_index * u.cm**2 / u.g + silicate_feature + water_feature\n", "\n", "# Create the Dust object.\n", - "d = Dust(lam=wavelengths[0,:], \n", - " amax=amax[:,0], \n", - " p=p[:,0], \n", - " abundances=(silicate_fraction[:,0], water_fraction[:,0]),\n", - " kabs=kappa_abs, \n", - " ksca=kappa_scat)" + "d = IsotropicDust(lam=wavelengths[0,:], \n", + " amax=amax[:,0], \n", + " p=p[:,0], \n", + " abundances=(silicate_fraction[:,0], water_fraction[:,0]),\n", + " kabs=kappa_abs, \n", + " ksca=kappa_scat)" ] }, { diff --git a/pinballrt/camera.py b/pinballrt/camera.py index dacfca1..876e2b5 100644 --- a/pinballrt/camera.py +++ b/pinballrt/camera.py @@ -27,6 +27,9 @@ def set_orientation(self, incl, pa, dpc): self.i = np.array([self.r*np.sin(self.incl)*np.cos(phi), \ self.r*np.sin(self.incl)*np.sin(phi), \ self.r*np.cos(self.incl)]) + + with wp.ScopedDevice(self.grid.device): + self.i_wp = wp.vec3(self.i) self.ex = np.array([-np.sin(phi), np.cos(phi), 0.0]) self.ey = np.array([-np.cos(self.incl)*np.cos(phi), \ diff --git a/pinballrt/dust.py b/pinballrt/dust.py index ef33cf4..a6b0b30 100644 --- a/pinballrt/dust.py +++ b/pinballrt/dust.py @@ -18,8 +18,29 @@ import os import zuko +from .utils import GridStruct, random_direction +from .photons import PhotonList + from torch.distributions.multivariate_normal import MultivariateNormal +import torch +from pytorch_lightning.plugins.io import TorchCheckpointIO +from lightning_fabric.utilities.cloud_io import get_filesystem +from typing import Any, Callable, Optional + +class CustomCheckpointIO(TorchCheckpointIO): + def save_checkpoint(self, checkpoint: dict, path: str, storage_options: Optional[Any] = None) -> None: + if storage_options is not None: + raise TypeError( + "`Trainer.save_checkpoint(..., storage_options=...)` with `storage_options` arg" + f" is not supported for `{self.__class__.__name__}`. Please implement your custom `CheckpointIO`" + " to define how you'd like to use `storage_options`." + ) + # Override save_checkpoint to use a specific protocol + fs = get_filesystem(path) + fs.makedirs(os.path.dirname(path), exist_ok=True) + torch.save(checkpoint, path, pickle_protocol=4) + wp.config.quiet = True default_fiducial_values = {"amax": 1.0*u.mm, "p": 3.5} @@ -262,6 +283,71 @@ def absorb(self, temperature): return direction, frequency + def update_photon_opacities(self, photon_list, iphotons, grid=None, inu=None): + nphotons = iphotons.size(0) + + if grid is not None and inu is not None: + wp.launch(kernel=self.set_photon_opacities_grid, + dim=(nphotons,), + inputs=[photon_list, grid, inu, iphotons]) + else: + wp.launch(kernel=self.set_photon_opacities, + dim=(nphotons,), + inputs=[photon_list, + self.ml_kabs(photon_list=photon_list, iphotons=iphotons), + self.ml_ksca(photon_list=photon_list, iphotons=iphotons), + iphotons]) + + @wp.kernel + def set_photon_opacities(photon_list: PhotonList, + kabs: wp.array(dtype=float), + ksca: wp.array(dtype=float), + iphotons: wp.array(dtype=int)): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + photon_list.kabs[ip] = kabs[i] + photon_list.ksca[ip] = ksca[i] + photon_list.albedo[ip] = ksca[i] / (kabs[i] + ksca[i]) + + @wp.kernel + def set_photon_opacities_grid(photon_list: PhotonList, + grid: GridStruct, + inu: int, + iphotons: wp.array(dtype=int)): # pragma: no cover + ip = iphotons[wp.tid()] + + ix, iy, iz = photon_list.indices[ip][0], photon_list.indices[ip][1], photon_list.indices[ip][2] + + photon_list.kabs[ip] = grid.kabs[inu, ix, iy, iz] + photon_list.ksca[ip] = grid.ksca[inu, ix, iy, iz] + photon_list.albedo[ip] = photon_list.ksca[ip] / (photon_list.kabs[ip] + photon_list.ksca[ip]) + + def set_grid_opacities(self, grid, frequency): + p = wp.to_torch(grid.p) + shape = p.shape + p = p.flatten() + amax = wp.to_torch(grid.amax).flatten() + abundances = tuple([wp.to_torch(grid.dust_abundances)[i].flatten() for i in range(len(self.abundances))]) + + kabs = [self.ml_kabs(p=p, + amax=amax, + abundances=abundances, + nu=torch.ones(np.prod(shape), dtype=torch.float32, + device=wp.device_to_torch(wp.get_device())) * \ + f.to(u.GHz).value) for f in frequency] + + grid.kabs = wp.from_torch(torch.concatenate(kabs).reshape((len(frequency),) + shape)) + + ksca = [self.ml_ksca(p=p, + amax=amax, + abundances=abundances, + nu=torch.ones(np.prod(shape), dtype=torch.float32, + device=wp.device_to_torch(wp.get_device())) * + f.to(u.GHz).value) for f in frequency] + + grid.ksca = wp.from_torch(torch.concatenate(ksca).reshape((len(frequency),) + shape)) + def random_nu_ml(self, p, amax, temperature, abundances=None): nphotons = temperature.size ksi = torch.rand(int(nphotons), dtype=torch.float32) @@ -360,21 +446,15 @@ def ml_step(self, photon_list, s, iphotons): return 10.**torch.clamp(test_x[:,0], self.log10_nu0_min, self.log10_nu0_max), 10.**test_x[:,1], 10.**test_x[:,2], test_x[:,3], test_x[:,4], torch.zeros(test_x.size(0)), test_x[:,5], test_x[:,6], torch.zeros(test_x.size(0)) - def initialize_model(self, model="random_nu", input_size=2, output_size=1, hidden_units=(48, 48, 48)): - if model == 'ml_step': - self.ml_step_model = NeuralSplineFlow(input_size, output_size, transforms=len(hidden_units), hidden_features=hidden_units[0]) - elif model == 'random_nu': - self.random_nu_model = MultiLayerPerceptron(input_size, output_size, hidden_units=hidden_units) - elif model == "kabs": - self.kabs_model = MultiLayerPerceptron(input_size, output_size, hidden_units=hidden_units) - elif model == "ksca": - self.ksca_model = MultiLayerPerceptron(input_size, output_size, hidden_units=hidden_units) - elif model == "pmo": - self.pmo_model = MultiLayerPerceptron(input_size, output_size, hidden_units=hidden_units) + def initialize_model(self, model="random_nu", model_type="MLP", input_size=2, output_size=1, hidden_units=(48, 48, 48)): + if model_type == 'flow': + setattr(self, f"{model}_model", NeuralSplineFlow(input_size, output_size, transforms=len(hidden_units), hidden_features=hidden_units[0])) + else: + setattr(self, f"{model}_model", MultiLayerPerceptron(input_size, output_size, hidden_units=hidden_units)) def learn(self, model="random_nu", nsamples=200000, test_split=0.1, valid_split=0.2, hidden_units=(48, 48, 48), tau_range=(3.0, 1e4), temperature_range=(0.1*u.K, 1e4*u.K), amax_range=(1*u.micron, 10.0*u.cm), p_range=(2.5, 4.5), - nu_range=None, overwrite=False): + nu_range=None, overwrite=False, checkpoint=True, pickle_protocol=2): """ Learn a model for either the random_nu function or the ml_step function. @@ -415,9 +495,11 @@ def learn(self, model="random_nu", nsamples=200000, test_split=0.1, valid_split= # Set up the NN if model == "random_nu": - input_size, output_size = self.ndims + 2, 1 + self.input_size, self.output_size = self.ndims + 2, 1 + self.model_type = "MLP" elif model == "ml_step": - input_size, output_size = 7, self.ndims + 3 + self.input_size, self.output_size = 7, self.ndims + 3 + self.model_type = "flow" if nu_range is None: nu_range = (self.nu.value.min(), self.nu.value.max()) @@ -432,17 +514,31 @@ def learn(self, model="random_nu", nsamples=200000, test_split=0.1, valid_split= self.p_max = p_range[1] self.log10_tau_cell_nu0_min = np.log10(tau_range[0]) self.log10_tau_cell_nu0_max = np.log10(tau_range[1]) - elif model in ["kabs", "ksca","pmo"]: - input_size, output_size = self.ndims + 1, 1 + elif model in ["kabs", "ksca", "g", "pmo"]: + self.input_size, self.output_size = self.ndims + 1, 1 + self.model_type = "MLP" - self.initialize_model(model=model, input_size=input_size, output_size=output_size, hidden_units=hidden_units) + self.initialize_model(model=model, model_type=self.model_type, input_size=self.input_size, output_size=self.output_size, hidden_units=hidden_units) # Wrap the model in lightning self.dustLM = DustLightningModule(getattr(self, model+"_model")) - self.trainer = pl.Trainer(max_epochs=0, callbacks=[pl.callbacks.ModelCheckpoint(save_last=True, - dirpath=f'{model}_lightning_logs')]) + if checkpoint: + callbacks = [pl.callbacks.ModelCheckpoint(save_last=True, + dirpath=f'{model}_lightning_logs')] + if pickle_protocol >= 4: + plugins = [CustomCheckpointIO()] + else: + plugins = None + else: + callbacks = None + plugins = None + + self.trainer = pl.Trainer(max_epochs=0, + enable_checkpointing=checkpoint, + callbacks=callbacks, + plugins=plugins) def fit(self, epochs=10, batch_size=100, num_workers=1, ckpt_path=None): ''' @@ -479,8 +575,10 @@ def test_model(self, plot=False): if self.current_model == "random_nu": self.plot_triangle_plots(model=self.current_model) - elif self.current_model in ["kabs", "ksca", "pmo"]: + elif self.current_model in ["kabs", "ksca", "g", "pmo"]: self.plot_opacity_model(model=self.current_model) + elif self.current_model == "scattering_phase_function": + self.plot_scattering_phase_function_model() else: #self.plot_ml_step() self.plot_triangle_plots(model=self.current_model) @@ -569,7 +667,10 @@ def prepare_data_opacity(self, model="kabs"): samples = np.concat((samples, np.expand_dims(np.repeat(np.expand_dims(np.log10(self.nu.to(u.GHz).value), axis=0), max(samples.shape[1], 1), axis=0), axis=0)), axis=0) samples = samples.reshape((samples.shape[0], -1)).T - targets = np.log10(getattr(self, model).flatten()) + if model == "g": + targets = getattr(self, model).flatten() + else: + targets = np.log10(getattr(self, model).flatten()) return samples, targets @@ -809,7 +910,7 @@ def plot_opacity_model(self, model='kabs', show_scipy_interpolation=False): else: index = np.random.randint(0, self.samples.shape[0], 1)[0] - if model in ["kabs", "ksca"]: + if model in ["kabs", "ksca", "g"]: nx = self.nu.size elif model in ["pmo"]: nx = self.temperature.size @@ -825,7 +926,10 @@ def plot_opacity_model(self, model='kabs', show_scipy_interpolation=False): log10_nu = np.log10(self.nu.to(u.GHz).value) log10_temperature = np.log10(self.temperature) - interpolated = np.log10(getattr(self, model)[index,:]) + if model == "g": + interpolated = getattr(self, model)[index,:] + else: + interpolated = np.log10(getattr(self, model)[index,:]) print_str = "" for dim in self.dims: @@ -842,7 +946,7 @@ def plot_opacity_model(self, model='kabs', show_scipy_interpolation=False): else: samples += (locals()[dim],) - if model in ["kabs", "ksca"]: + if model in ["kabs", "ksca", "g"]: samples = np.vstack(samples + (log10_nu,)).T plot_x = log10_lam else: @@ -918,11 +1022,11 @@ def plot_triangle_plots(self, model="ml_step", nsamples=200000, batch_size=100, self.num_workers = num_workers if self.trainer is not None: - if model == "ml_step": + if model in ["ml_step"]: X_pred = self.trainer.predict(self.dustLM, datamodule=self) X_pred = torch.cat(X_pred) y_pred = torch.cat([batch[1] for batch in self.predict_dataloader()]) - elif model == "random_nu": + elif model in ["random_nu", "random_direction"]: y_pred = self.trainer.predict(self.dustLM, datamodule=self) y_pred = torch.cat(y_pred) X_pred = torch.cat([batch[0] for batch in self.predict_dataloader()]) @@ -949,6 +1053,17 @@ def plot_triangle_plots(self, model="ml_step", nsamples=200000, batch_size=100, features = np.array(features + ("log10_temperature", "ksi")) targets = np.array(["log10_nu"]) + y_true = torch.unsqueeze(y_true, 1) + elif model == "random_direction": + features = () + for dim in self.dims: + if dim == "abundances": + features += tuple([f"abundance_{i}" for i in range(len(self.abundances))]) + else: + features += (dim,) + features = np.array(features + ("log10_nu", "ksi")) + targets = np.array(["theta"]) + y_true = torch.unsqueeze(y_true, 1) if isinstance(plot_columns, str) and plot_columns == 'all': @@ -1082,6 +1197,522 @@ def save(self, filename): def copy(self, device="cpu"): return load(self.state_dict(), device=device) +class IsotropicDust(Dust): + def scatter(self, photon_list, iphotons): + nphotons = iphotons.size(0) + + wp.launch(kernel=random_direction, + dim=(nphotons,), + inputs=[photon_list.direction, iphotons, np.random.randint(0, 100000)]) + + def update_photon_scattering_phase_function(self, photon_list, direction, iphotons): + nphotons = iphotons.size(0) + + wp.launch(kernel=self.scattering_phase_function_wp, + dim=(nphotons,), + inputs=[photon_list, direction, iphotons]) + + @wp.kernel + def scattering_phase_function_wp(photon_list: PhotonList, + direction: wp.vec3, + iphotons: wp.array(dtype=int)): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + photon_list.scattering_phase_function[ip] = 1. + + +class HenyeyGreensteinDust(Dust): + def __init__(self, lam=None, kabs=None, ksca=None, g=None, amax=None, p=None, abundances=(), device="cpu", ntemperatures=1000, + fiducial_values={}): + """ + Initialize the Henyey-Greenstein dust model. + + Parameters + ---------- + lam : numpy.ndarray + The wavelengths to use for the dust properties. + kabs : numpy.ndarray + The absorption opacities to use for the dust properties. + ksca : numpy.ndarray + The scattering opacities to use for the dust properties. + g : numpy.ndarray + The Henyey-Greenstein asymmetry parameter to use for the dust properties. + amax : float + The maximum grain size to use for the dust properties. + p : float + The power-law index for the grain size distribution. + device : str + The device to place the dust properties on ("cpu" or "cuda"). + """ + super().__init__(lam=lam, kabs=kabs, ksca=ksca, amax=amax, p=p, abundances=abundances, device=device, + ntemperatures=ntemperatures, fiducial_values=fiducial_values) + + self.g = g + + def to_device(self, device): + super().to_device(device) + + for model in ["g"]: + if hasattr(self, f"{model}_model"): + getattr(self, f"{model}_model").to(device) + if hasattr(self, f"{model}_x_scaler"): + getattr(self, f"{model}_x_scaler").to(device) + if hasattr(self, f"{model}_y_scaler"): + getattr(self, f"{model}_y_scaler").to(device) + + def scatter(self, photon_list, iphotons): + nphotons = iphotons.size(0) + + wp.launch(kernel=self.random_direction, + dim=(nphotons,), + inputs=[photon_list.direction, photon_list.g, iphotons, np.random.randint(0, 100000)]) + + @wp.kernel + def random_direction(direction: wp.array(dtype=wp.vec3), + g: wp.array(dtype=float), + iphotons: wp.array(dtype=int), + seed: int): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + rng = wp.rand_init(seed, i) + + cost = (1. + g[ip]**2. - ((1. - g[ip]**2.)/(1. - g[ip] + 2.*g[ip]*wp.randf(rng)))**2.) / (2. * g[ip]) + theta = wp.acos(cost) + phi = 2.*np.pi*wp.randf(rng) + + rpy_quat1 = wp.quat_rpy(phi, 0., 0.) + rpy_quat2 = wp.quat_rpy(0., theta, 0.) + direction_quat = wp.quat_between_vectors(wp.vec3(1., 0., 0.), direction[ip]) + total_quat = direction_quat * rpy_quat1 * rpy_quat2 + + direction[ip] = wp.quat_rotate(total_quat, wp.vec3(1., 0., 0.)) + + def update_photon_scattering_phase_function(self, photon_list, direction, iphotons): + nphotons = iphotons.size(0) + + wp.launch(kernel=self.scattering_phase_function_heneygreenstein_wp, + dim=(nphotons,), + inputs=[photon_list, direction, iphotons]) + + @wp.kernel + def scattering_phase_function_heneygreenstein_wp(photon_list: PhotonList, + direction: wp.vec3, + iphotons: wp.array(dtype=int)): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + mu = wp.dot(photon_list.direction[ip], direction) + + photon_list.scattering_phase_function[ip] = (1. - photon_list.g[ip]**2.) / (1. + photon_list.g[ip]**2. - 2. * photon_list.g[ip] * mu) + + def ml_g(self, p=None, amax=None, nu=None, abundances=None, photon_list=None, iphotons=None): + if photon_list is not None: + p = wp.to_torch(photon_list.p) + amax = wp.to_torch(photon_list.amax) + if photon_list.dust_abundances is not None: + abundances = wp.to_torch(photon_list.dust_abundances) + + if nu is None: + nu = wp.to_torch(photon_list.frequency) + + if iphotons is not None: + nu = nu[iphotons] + p = p[iphotons] + amax = amax[iphotons] + if abundances is not None: + abundances = abundances[iphotons] + else: + if nu.size(0) != p.size(0): + p = p[iphotons] + amax = amax[iphotons] + if abundances is not None: + abundances = abundances[iphotons] + + abundances = tuple([abundances[:,i] for i in range(len(self.abundances))]) + + if amax is not None: + log10_amax = torch.log10(amax) + + samples = () + for dim in self.dims: + if dim == "abundances" and abundances is not None: + samples += abundances + else: + samples += (eval(dim),) + samples += (torch.log10(nu),) + samples = torch.transpose(torch.vstack(samples), 0, 1) + + g = self.g_y_scaler.inverse_transform(self.g_model(self.g_x_scaler.transform(samples))).detach().flatten() + + return g + + def prepare_data_g(self): + return self.prepare_data_opacity(model="g") + + def update_photon_opacities(self, photon_list, iphotons, grid=None, inu=None): + nphotons = iphotons.size(0) + + if grid is not None and inu is not None: + wp.launch(kernel=self.set_photon_opacities_grid, + dim=(nphotons,), + inputs=[photon_list, grid, inu, iphotons]) + else: + wp.launch(kernel=self.set_photon_opacities, + dim=(nphotons,), + inputs=[photon_list, + self.ml_kabs(photon_list=photon_list, iphotons=iphotons), + self.ml_ksca(photon_list=photon_list, iphotons=iphotons), + self.ml_g(photon_list=photon_list, iphotons=iphotons), + iphotons]) + + @wp.kernel + def set_photon_opacities(photon_list: PhotonList, + kabs: wp.array(dtype=float), + ksca: wp.array(dtype=float), + g: wp.array(dtype=float), + iphotons: wp.array(dtype=int)): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + photon_list.kabs[ip] = kabs[i] + photon_list.ksca[ip] = ksca[i] + photon_list.g[ip] = g[i] + photon_list.albedo[ip] = ksca[i] / (kabs[i] + ksca[i]) + + @wp.kernel + def set_photon_opacities_grid(photon_list: PhotonList, + grid: GridStruct, + inu: int, + iphotons: wp.array(dtype=int)): # pragma: no cover + ip = iphotons[wp.tid()] + + ix, iy, iz = photon_list.indices[ip][0], photon_list.indices[ip][1], photon_list.indices[ip][2] + + photon_list.kabs[ip] = grid.kabs[inu, ix, iy, iz] + photon_list.ksca[ip] = grid.ksca[inu, ix, iy, iz] + photon_list.g[ip] = grid.g[inu, ix, iy, iz] + photon_list.albedo[ip] = photon_list.ksca[ip] / (photon_list.kabs[ip] + photon_list.ksca[ip]) + + def set_grid_opacities(self, grid, frequency): + super().set_grid_opacities(grid, frequency) + + p = wp.to_torch(grid.p) + shape = p.shape + p = p.flatten() + amax = wp.to_torch(grid.amax).flatten() + abundances = tuple([wp.to_torch(grid.dust_abundances)[i].flatten() for i in range(len(self.abundances))]) + + g = [self.ml_g(p=p, + amax=amax, + abundances=abundances, + nu=torch.ones(np.prod(shape), dtype=torch.float32, + device=wp.device_to_torch(wp.get_device())) * \ + f.to(u.GHz).value) for f in frequency] + + grid.g = wp.from_torch(torch.concatenate(g).reshape((len(frequency),) + shape)) + + def state_dict(self): + state_dict = super().state_dict() + + state_dict["dust_properties"]["g"] = self.g + + if hasattr(self, "g_model"): + state_dict["g_state_dict"] = self.g_model.state_dict() + state_dict["g_x_scaler"] = self.g_x_scaler.state_dict() + state_dict["g_y_scaler"] = self.g_y_scaler.state_dict() + + return state_dict + +class GeneralDust(Dust): + def __init__(self, lam=None, kabs=None, ksca=None, scattering_phase_function=None, theta=None, amax=None, + p=None, abundances=(), device="cpu", ntemperatures=1000, fiducial_values={}): + """ + Initialize the General dust model. + + Parameters + ---------- + lam : numpy.ndarray + The wavelengths to use for the dust properties. + kabs : numpy.ndarray + The absorption opacities to use for the dust properties. + ksca : numpy.ndarray + The scattering opacities to use for the dust properties. + scattering_phase_function : numpy.ndarray + The scattering phase function to use for the dust properties, as a function of theta. + theta : numpy.ndarray + The angles at which the scattering phase function is tabulated. + amax : float + The maximum grain size to use for the dust properties. + p : float + The power-law index for the grain size distribution. + device : str + The device to place the dust properties on ("cpu" or "cuda"). + """ + super().__init__(lam=lam, kabs=kabs, ksca=ksca, amax=amax, p=p, abundances=abundances, device=device, + ntemperatures=ntemperatures, fiducial_values=fiducial_values) + + # Ensure that the scattering phase function is normalized for each combination of p, amax, and nu. + + scattering_phase_function = -1. * scattering_phase_function / \ + np.expand_dims(scipy.integrate.trapezoid(scattering_phase_function, + np.cos(theta), axis=-1), axis=-1) + + self.scattering_phase_function = scattering_phase_function + self.theta = theta + + def to_device(self, device): + super().to_device(device) + + def scatter(self, photon_list, iphotons): + nphotons = iphotons.size(0) + + p = wp.to_torch(photon_list.p) + amax = wp.to_torch(photon_list.amax) + frequency = wp.to_torch(photon_list.frequency) + if photon_list.dust_abundances is not None: + abundances = wp.to_torch(photon_list.dust_abundances) + if iphotons is not None: + p = p[iphotons] + amax = amax[iphotons] + frequency = frequency[iphotons] + if photon_list.dust_abundances is not None: + abundances = abundances[iphotons] + + abundances = tuple([abundances[:,i] for i in range(len(self.abundances))]) + + nphotons = iphotons.size(0) + ksi = torch.rand(int(nphotons), device=wp.device_to_torch(wp.get_device()), dtype=torch.float32) + ksi = torch.clamp(torch.arctanh(2*ksi - 1.), min=-8.6643, max=8.6643) + + if amax is not None: + log10_amax = torch.log10(amax) + + samples = () + for dim in self.dims: + if dim == "abundances" and abundances is not None: + samples += abundances + else: + samples += (eval(dim),) + samples += (torch.log10(frequency), ksi) + + samples = torch.transpose(torch.vstack(samples), 0, 1) + + test_x = self.random_direction_x_scaler.transform(samples) + + theta = self.random_direction_y_scaler.inverse_transform(self.random_direction_model(test_x).detach()).flatten() + + wp.launch(kernel=self.random_direction, + dim=(nphotons,), + inputs=[photon_list.direction, + wp.from_torch(theta), + iphotons, + np.random.randint(0, 100000)]) + + @wp.kernel + def random_direction(direction: wp.array(dtype=wp.vec3), + theta: wp.array(dtype=float), + iphotons: wp.array(dtype=int), + seed: int): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + rng = wp.rand_init(seed, i) + + phi = 2.*np.pi*wp.randf(rng) + + rpy_quat1 = wp.quat_rpy(phi, 0., 0.) + rpy_quat2 = wp.quat_rpy(0., theta[i], 0.) + direction_quat = wp.quat_between_vectors(wp.vec3(1., 0., 0.), direction[ip]) + total_quat = direction_quat * rpy_quat1 * rpy_quat2 + + direction[ip] = wp.quat_rotate(total_quat, wp.vec3(1., 0., 0.)) + + def update_photon_scattering_phase_function(self, photon_list, direction, iphotons): + nphotons = iphotons.size(0) + + theta = torch.acos((wp.to_torch(photon_list.direction)[iphotons] * torch.tensor(wp.array(direction))).sum(axis=1)) + + scattering_phase_function = self.ml_scattering_phase_function(photon_list=photon_list, iphotons=iphotons, theta=theta) + + wp.launch(kernel=self.scattering_phase_function_general_dust_wp, + dim=(nphotons,), + inputs=[photon_list, + wp.from_torch(scattering_phase_function), + iphotons]) + + @wp.kernel + def scattering_phase_function_general_dust_wp(photon_list: PhotonList, + scattering_phase_function: wp.array(dtype=float), + iphotons: wp.array(dtype=int)): # pragma: no cover + + i = wp.tid() + ip = iphotons[i] + + photon_list.scattering_phase_function[ip] = 2. * scattering_phase_function[i] + + def ml_scattering_phase_function(self, p=None, amax=None, nu=None, theta=None, photon_list=None, iphotons=None): + if photon_list is not None: + p = wp.to_torch(photon_list.p) + amax = wp.to_torch(photon_list.amax) + if photon_list.dust_abundances is not None: + abundances = wp.to_torch(photon_list.dust_abundances) + + if nu is None: + nu = wp.to_torch(photon_list.frequency) + + if iphotons is not None: + nu = nu[iphotons] + p = p[iphotons] + amax = amax[iphotons] + if abundances is not None: + abundances = abundances[iphotons] + else: + if nu.size(0) != p.size(0): + p = p[iphotons] + amax = amax[iphotons] + if abundances is not None: + abundances = abundances[iphotons] + + abundances = tuple([abundances[:,i] for i in range(len(self.abundances))]) + + if amax is not None: + log10_amax = torch.log10(amax) + + samples = () + for dim in self.dims: + if dim == "abundances" and abundances is not None: + samples += abundances + else: + samples += (eval(dim),) + samples += (torch.log10(nu), theta) + samples = torch.transpose(torch.vstack(samples), 0, 1) + + scattering_phase_function = 10.**self.scattering_phase_function_y_scaler.inverse_transform( + self.scattering_phase_function_model(self.scattering_phase_function_x_scaler.transform(samples))).\ + detach().flatten() + + return scattering_phase_function + + def learn(self, model="random_nu", **kwargs): + if model == "scattering_phase_function": + self.input_size, self.output_size = self.ndims + 2, 1 + self.model_type = "MLP" + elif model == "random_direction": + self.input_size, self.output_size = self.ndims + 2, 1 + self.model_type = "MLP" + + super().learn(model=model, **kwargs) + + def prepare_data_scattering_phase_function(self): + samples = np.repeat(np.repeat(np.expand_dims(np.moveaxis(self.samples, 0, 1), (-1, -2)), self.lam.size, axis=-2), self.theta.size, axis=-1) + self.original_indices = np.tile(np.expand_dims(np.arange(self.samples.shape[0]), axis=(-1, -2)), (1, self.nu.size, self.theta.size)).flatten() + + samples = np.concat((samples, + np.tile(np.expand_dims(np.log10(self.nu.to(u.GHz).value), + axis=(0,1,-1)), + (1, samples.shape[1], 1, self.theta.size)), + np.tile(np.expand_dims(self.theta.to(u.radian).value, + axis=(0,1,2)), + (1, samples.shape[1], self.lam.size, 1))), axis=0) + samples = samples.reshape((samples.shape[0], -1)).T + + targets = np.log10(self.scattering_phase_function.flatten()) + + return samples, targets + + def prepare_data_random_direction(self): + ksi = -1. * scipy.integrate.cumulative_trapezoid(self.scattering_phase_function, np.cos(self.theta), initial=0, axis=-1) + + samples = np.repeat(np.repeat(np.expand_dims(np.moveaxis(self.samples, 0, 1), (-1, -2)), self.lam.size, axis=-2), self.theta.size, axis=-1) + self.original_indices = np.tile(np.expand_dims(np.arange(self.samples.shape[0]), axis=(-1, -2)), (1, self.nu.size, self.theta.size)).flatten() + + samples = np.concat((samples, + np.tile(np.expand_dims(np.log10(self.nu.to(u.GHz).value), + axis=(0,1,-1)), + (1, samples.shape[1], 1, self.theta.size)), + np.expand_dims(ksi, axis=(0,))), axis=0) + + targets = np.tile(np.expand_dims(self.theta.to(u.radian).value, axis=(0,1,2)), (1, samples.shape[1], self.lam.size, 1)) + + samples = samples.reshape((samples.shape[0], -1)).T + targets = targets.flatten() + self.nsamples = samples.shape[0] + + samples = samples.astype(np.float32) + + good = np.logical_not(np.logical_or(samples[:,-1] < 2e-8, samples[:,-1] == 1)) + + samples, targets = samples[good,:], targets[good] + self.original_indices = self.original_indices[good] + + samples[:,-1] = 2*samples[:,-1] - 1 + samples[:,-1] = np.arctanh(samples[:,-1]) + + return samples, targets + + def plot_scattering_phase_function_model(self): + import matplotlib.pyplot as plt + + index_samples = np.random.randint(0, self.samples.shape[0], 1)[0] + + if "log10_amax" in self.dims: + log10_amax = np.repeat(np.log10(self.amax[index_samples].to(u.cm).value), self.theta.size) + if "p" in self.dims: + p = np.repeat(self.p[index_samples], self.theta.size) + if "abundances" in self.dims: + abundances = tuple([np.repeat(a[index_samples], self.theta.size) for a in self.abundances]) + + index_nu = np.random.randint(0, self.nu.size, 1)[0] + + log10_lam = np.repeat(np.log10(self.lam[index_nu].value), self.theta.size) + log10_nu = np.repeat(np.log10(self.nu[index_nu].to(u.GHz).value), self.theta.size) + + interpolated = np.log10(self.scattering_phase_function[index_samples, index_nu, :]) + + print_str = "" + for dim in self.dims: + if dim == "abundances": + print_str += f"{dim}: {[abundances[i][0] for i in range(len(abundances))]}, " + else: + print_str += f"{dim}: {locals()[dim][0]}, " + print(print_str) + + samples = () + for dim in self.dims: + if dim == "abundances": + samples += abundances + else: + samples += (locals()[dim],) + + samples = np.vstack(samples + (log10_nu, self.theta.to(u.radian).value)).T + plot_x = self.theta + + nned = self.scattering_phase_function_y_scaler.inverse_transform(self.scattering_phase_function_model(self.scattering_phase_function_x_scaler.transform(torch.tensor(samples, dtype=torch.float32)))).detach().numpy() + + plt.plot(plot_x, interpolated) + plt.plot(plot_x, nned) + plt.show() + + def state_dict(self): + state_dict = super().state_dict() + + state_dict["dust_properties"]["scattering_phase_function"] = self.scattering_phase_function + state_dict["dust_properties"]["theta"] = self.theta + + if hasattr(self, "scattering_phase_function_model"): + state_dict["scattering_phase_function_state_dict"] = self.scattering_phase_function_model.state_dict() + state_dict["scattering_phase_function_x_scaler"] = self.scattering_phase_function_x_scaler.state_dict() + state_dict["scattering_phase_function_y_scaler"] = self.scattering_phase_function_y_scaler.state_dict() + + if hasattr(self, "random_direction_model"): + state_dict["random_direction_state_dict"] = self.random_direction_model.state_dict() + state_dict["random_direction_x_scaler"] = self.random_direction_x_scaler.state_dict() + state_dict["random_direction_y_scaler"] = self.random_direction_y_scaler.state_dict() + + return state_dict + def suggest_opacity_sampling(nsamples, p_range=None, amax_range=None, n_dust_subspecies=1, mode='lhs'): """ Suggest samples for learning the opacity as a function of the dust properties using Latin Hypercube sampling. @@ -1168,17 +1799,22 @@ def load(filename, device="cpu"): elif isinstance(filename, Dust): state_dict = filename.state_dict() - d = Dust(**state_dict["dust_properties"], device=device) + if "g" in state_dict["dust_properties"]: + d = HenyeyGreensteinDust(**state_dict["dust_properties"], device=device) + elif "scattering_phase_function" in state_dict["dust_properties"]: + d = GeneralDust(**state_dict["dust_properties"], device=device) + else: + d = IsotropicDust(**state_dict["dust_properties"], device=device) - for attr in ["kabs", "ksca", "pmo", "random_nu"]: + for attr in ["kabs", "ksca", "pmo", "random_nu", "g", "scattering_phase_function"]: if f"{attr}_state_dict" in state_dict: - if attr == "random_nu": + if attr in ["random_nu", "scattering_phase_function"]: input_size = d.ndims + 2 else: input_size = d.ndims + 1 hidden_units = [state_dict[f'{attr}_state_dict'][key].shape[0] for key in state_dict[f'{attr}_state_dict'] if 'bias' in key][0:-1] - d.initialize_model(model=attr, input_size=input_size, output_size=1, hidden_units=hidden_units) + d.initialize_model(model=attr, model_type="MLP", input_size=input_size, output_size=1, hidden_units=hidden_units) getattr(d, f'{attr}_model').load_state_dict(state_dict[f'{attr}_state_dict']) setattr(d, f'{attr}_x_scaler', StandardScaler()) @@ -1186,10 +1822,22 @@ def load(filename, device="cpu"): setattr(d, f'{attr}_y_scaler', StandardScaler()) getattr(d, f'{attr}_y_scaler').load_state_dict(state_dict[f"{attr}_y_scaler"]) + if "random_direction_state_dict" in state_dict: + hidden_units = (tuple([state_dict['random_direction_state_dict'][key].size(1) for key in state_dict['random_direction_state_dict'] if '0.hyper' in key and 'weight' in key and '0.weight' not in key]),) * len([state_dict['random_direction_state_dict'][key].size(0) for key in state_dict['random_direction_state_dict'] if '0.weight' in key]) + + d.initialize_model(model="random_direction", model_type="flow", input_size=1, output_size=3, hidden_units=hidden_units) + + d.random_direction_model.load_state_dict(state_dict['random_direction_state_dict']) + + d.random_direction_x_scaler = StandardScaler() + d.random_direction_x_scaler.load_state_dict(state_dict["random_direction_x_scaler"]) + d.random_direction_y_scaler = StandardScaler() + d.random_direction_y_scaler.load_state_dict(state_dict["random_direction_y_scaler"]) + if "ml_step_state_dict" in state_dict: hidden_units = (tuple([state_dict['ml_step_state_dict'][key].size(1) for key in state_dict['ml_step_state_dict'] if '0.hyper' in key and 'weight' in key and '0.weight' not in key]),) * len([state_dict['ml_step_state_dict'][key].size(0) for key in state_dict['ml_step_state_dict'] if '0.weight' in key]) - d.initialize_model(model="ml_step", input_size=7, output_size=5, hidden_units=hidden_units) + d.initialize_model(model="ml_step", model_type="flow", input_size=7, output_size=5, hidden_units=hidden_units) d.ml_step_model.load_state_dict(state_dict['ml_step_state_dict']) diff --git a/pinballrt/grids.py b/pinballrt/grids.py index 1980c4e..dedd709 100644 --- a/pinballrt/grids.py +++ b/pinballrt/grids.py @@ -10,7 +10,7 @@ import time from tqdm.auto import tqdm -from .utils import GridStruct, EPSILON, equal, equal_zero, planck_function +from .utils import GridStruct, EPSILON, equal, equal_zero, planck_function, random_direction class Grid: def __init__(self, _w1, _w2, _w3, device='cpu'): @@ -330,34 +330,12 @@ def photon_cell_properties(photon_list: PhotonList, @wp.kernel def update_frequency(photon_list: PhotonList, frequency: wp.array(dtype=float), - kabs: wp.array(dtype=float), - ksca: wp.array(dtype=float), iphotons: wp.array(dtype=int)): # pragma: no cover i = wp.tid() ip = iphotons[i] photon_list.frequency[ip] = frequency[i] - photon_list.kabs[ip] = kabs[i] - photon_list.ksca[ip] = ksca[i] - photon_list.albedo[ip] = ksca[i] / (kabs[i] + ksca[i]) - - @wp.kernel - def random_direction(direction: wp.array(dtype=wp.vec3), - iphotons: wp.array(dtype=int), - seed: int): # pragma: no cover - i = wp.tid() - ip = iphotons[i] - - rng = wp.rand_init(seed, i) - - cost = -1. + 2.*wp.randf(rng) - sint = wp.sqrt(1.-cost**2.) - phi = 2.*np.pi*wp.randf(rng) - - direction[ip][0] = sint*np.cos(phi) - direction[ip][1] = sint*np.sin(phi) - direction[ip][2] = cost @wp.kernel def random_tau(photon_list: PhotonList, @@ -381,13 +359,9 @@ def random_absorb(photon_list: PhotonList, photon_list.absorb[ip] = wp.randf(rng) > photon_list.albedo[ip] - def interact(self, photon_list: PhotonList, absorb, iabsorb, interact, iphotons, scattering=False, learning=False): + def interact(self, photon_list: PhotonList, absorb, iabsorb, interact, iphotons, iscatter, scattering=False, learning=False): nphotons = iphotons.size(0) - wp.launch(kernel=self.random_direction, - dim=(nphotons,), - inputs=[photon_list.direction, iphotons, np.random.randint(0, 100000)]) - t1 = time.time() nabsorb = iabsorb.size(0) #if not scattering and nabsorb > 0: @@ -398,6 +372,12 @@ def interact(self, photon_list: PhotonList, absorb, iabsorb, interact, iphotons, t2 = time.time() photon_temperature_time = t2 - t1 + wp.launch(kernel=random_direction, + dim=(nabsorb,), + inputs=[photon_list.direction, iabsorb, np.random.randint(0, 100000)]) + + self.dust.scatter(photon_list, iscatter) + t1 = time.time() if not scattering and nabsorb > 0: new_frequency = self.dust.random_nu(photon_list, subset=absorb) @@ -408,9 +388,9 @@ def interact(self, photon_list: PhotonList, absorb, iabsorb, interact, iphotons, if not scattering and nabsorb > 0: wp.launch(kernel=self.update_frequency, dim=(nabsorb,), - inputs=[photon_list, new_frequency, - self.dust.ml_kabs(photon_list=photon_list, nu=wp.to_torch(new_frequency), iphotons=iabsorb), - self.dust.ml_ksca(photon_list=photon_list, nu=wp.to_torch(new_frequency), iphotons=iabsorb), iabsorb]) + inputs=[photon_list, new_frequency, iabsorb]) + + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iabsorb) t2 = time.time() dust_interpolation_time = t2 - t1 @@ -540,7 +520,9 @@ def ml_step(self, photon_list, s, iphotons): wp.launch(kernel=self.update_frequency, dim=(nphotons,), - inputs=[photon_list, wp.from_torch(frequency), self.dust.ml_kabs(photon_list=photon_list, nu=wp.from_torch(frequency, iphotons=iphotons)), self.dust.ml_ksca(photon_list=photon_list, nu=wp.from_torch(frequency, iphotons=iphotons)), iphotons]) + inputs=[photon_list, wp.from_torch(frequency), iphotons]) + + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iphotons) wp.launch(kernel=self.ml_rotate_direction, dim=(nphotons,), @@ -552,18 +534,6 @@ def ml_step(self, photon_list, s, iphotons): return wp.from_torch(direction_yaw), wp.from_torch(direction_pitch), wp.from_torch(direction_roll) - @wp.kernel - def set_photon_opacities(photon_list: PhotonList, - kabs: wp.array(dtype=float), - ksca: wp.array(dtype=float), - iphotons: wp.array(dtype=int)): # pragma: no cover - i = wp.tid() - ip = iphotons[i] - - photon_list.kabs[ip] = kabs[i] - photon_list.ksca[ip] = ksca[i] - photon_list.albedo[ip] = ksca[i] / (kabs[i] + ksca[i]) - def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning=False, debug=False, timing={}, position=0, time_limit=np.inf): with wp.ScopedDevice(self.device): nphotons = photon_list.position.numpy().shape[0] @@ -595,6 +565,7 @@ def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning photon_list.kabs = wp.zeros(nphotons, dtype=float) photon_list.ksca = wp.zeros(nphotons, dtype=float) + photon_list.g = wp.zeros(nphotons, dtype=float) photon_list.albedo = wp.zeros(nphotons, dtype=float) photon_list.absorb = wp.zeros(nphotons, dtype=bool) @@ -611,12 +582,7 @@ def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning nphotons = iphotons.size(0) t1 = time.time() - wp.launch(kernel=self.set_photon_opacities, - dim=(nphotons,), - inputs=[photon_list, - wp.from_torch(self.dust.ml_kabs(photon_list=photon_list, iphotons=iphotons)), - wp.from_torch(self.dust.ml_ksca(photon_list=photon_list, iphotons=iphotons)), - iphotons]) + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iphotons) t2 = time.time() dust_interpolation_time += t2 - t1 @@ -736,7 +702,9 @@ def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning interaction_indices = iphotons_original[interaction] absorb = torch.logical_and(interaction, wp.to_torch(photon_list.absorb)) absorb_indices = iphotons_original[absorb] - tmp_photon_temp_time, tmp_dust_interpolation_time, tmp_photon_loc_time, tmp_absorb_random_nu_time = self.interact(photon_list, absorb, absorb_indices, interaction, interaction_indices, learning=learning) + scatter = torch.logical_and(interaction, wp.to_torch(photon_list.absorb) == False) + scatter_indices = iphotons_original[scatter] + tmp_photon_temp_time, tmp_dust_interpolation_time, tmp_photon_loc_time, tmp_absorb_random_nu_time = self.interact(photon_list, absorb, absorb_indices, interaction, interaction_indices, scatter_indices, learning=learning) t2 = time.time() absorb_time += t2 - t1 - tmp_dust_interpolation_time - tmp_photon_loc_time absorb_random_nu_time += tmp_absorb_random_nu_time @@ -759,12 +727,7 @@ def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning inputs=[photon_list, self.grid, iphotons, self.n_dust_abundances]) t1 = time.time() - wp.launch(kernel=self.set_photon_opacities, - dim=(nphotons,), - inputs=[photon_list, - wp.from_torch(self.dust.ml_kabs(photon_list=photon_list, iphotons=iphotons)), - wp.from_torch(self.dust.ml_ksca(photon_list=photon_list, iphotons=iphotons)), - iphotons]) + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iphotons) t2 = time.time() dust_interpolation_time += t2 - t1 @@ -785,38 +748,9 @@ def propagate_photons(self, photon_list: PhotonList, use_ml_step=False, learning def set_grid_opacities(self, frequency): with wp.ScopedDevice(self.device): - p = wp.to_torch(self.grid.p).flatten() - amax = wp.to_torch(self.grid.amax).flatten() - abundances = tuple([wp.to_torch(self.grid.dust_abundances)[i].flatten() for i in range(self.n_dust_abundances)]) + self.dust.set_grid_opacities(self.grid, frequency) - kabs = [self.dust.ml_kabs(p=p, amax=amax, abundances=abundances, nu=torch.ones(np.prod(self.shape), - dtype=torch.float32, - device=wp.device_to_torch(wp.get_device())) * \ - f.to(u.GHz).value) for f in frequency] - - self.grid.kabs = wp.from_torch(torch.concatenate(kabs).reshape((len(frequency),) + self.shape)) - - ksca = [self.dust.ml_ksca(p=p, amax=amax, abundances=abundances, nu=torch.ones(np.prod(self.shape), - dtype=torch.float32, - device=wp.device_to_torch(wp.get_device())) * \ - f.to(u.GHz).value) for f in frequency] - - self.grid.ksca = wp.from_torch(torch.concatenate(ksca).reshape((len(frequency),) +self.shape)) - - @wp.kernel - def update_photon_opacities(photon_list: PhotonList, - grid: GridStruct, - inu: int, - iphotons: wp.array(dtype=int)): # pragma: no cover - ip = iphotons[wp.tid()] - - ix, iy, iz = photon_list.indices[ip][0], photon_list.indices[ip][1], photon_list.indices[ip][2] - - photon_list.kabs[ip] = grid.kabs[inu, ix, iy, iz] - photon_list.ksca[ip] = grid.ksca[inu, ix, iy, iz] - photon_list.albedo[ip] = photon_list.ksca[ip] / (photon_list.kabs[ip] + photon_list.ksca[ip]) - - def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug=False, timing={}, position=0): + def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, camera_direction: wp.vec3, debug=False, timing={}, position=0): with wp.ScopedDevice(self.device): nphotons = photon_list.position.numpy().shape[0] iphotons_original = torch.arange(nphotons, dtype=torch.int32, device=wp.device_to_torch(wp.get_device())) @@ -841,8 +775,10 @@ def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug= photon_list.kabs = wp.zeros(nphotons, dtype=float) photon_list.ksca = wp.zeros(nphotons, dtype=float) + photon_list.g = wp.zeros(nphotons, dtype=float) photon_list.albedo = wp.zeros(nphotons, dtype=float) photon_list.absorb = wp.zeros(nphotons, dtype=bool) + photon_list.scattering_phase_function = wp.zeros(nphotons, dtype=float) progress_bar = tqdm(total=nphotons, position=position, leave=True) @@ -857,9 +793,7 @@ def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug= nphotons = iphotons.size(0) t1 = time.time() - wp.launch(kernel=self.update_photon_opacities, - dim=(nphotons,), - inputs=[photon_list, self.grid, inu, iphotons]) + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iphotons, grid=self.grid, inu=inu) t2 = time.time() dust_interpolation_time += t2 - t1 @@ -888,6 +822,10 @@ def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug= s = torch.minimum(wp.to_torch(s1), wp.to_torch(s2)) + self.dust.update_photon_scattering_phase_function(photon_list=photon_list, + direction=camera_direction, + iphotons=iphotons) + t1 = time.time() wp.launch(kernel=self.deposit_scattering, dim=(nphotons,), @@ -929,7 +867,9 @@ def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug= interaction_indices = iphotons_original[interaction] absorb = torch.logical_and(interaction, wp.to_torch(photon_list.absorb)) absorb_indices = iphotons_original[absorb] - tmp_photon_temp_time, tmp_dust_interpolation_time, tmp_photon_loc_time, tmp_absorb_random_nu_time = self.interact(photon_list, absorb, absorb_indices, interaction, interaction_indices, scattering=True) + scatter = torch.logical_and(interaction, wp.to_torch(photon_list.absorb) == False) + scatter_indices = iphotons_original[scatter] + tmp_photon_temp_time, tmp_dust_interpolation_time, tmp_photon_loc_time, tmp_absorb_random_nu_time = self.interact(photon_list, absorb, absorb_indices, interaction, interaction_indices, scatter_indices, scattering=True) t2 = time.time() absorb_time += t2 - t1 - tmp_photon_loc_time #absorb_time += tmp_time @@ -948,9 +888,7 @@ def propagate_photons_scattering(self, photon_list: PhotonList, inu: int, debug= inputs=[photon_list, self.grid, iphotons, self.n_dust_abundances]) t1 = time.time() - wp.launch(kernel=self.update_photon_opacities, - dim=(nphotons,), - inputs=[photon_list, self.grid, inu, iphotons]) + self.dust.update_photon_opacities(photon_list=photon_list, iphotons=iphotons, grid=self.grid, inu=inu) t2 = time.time() dust_interpolation_time += t2 - t1 @@ -1117,18 +1055,6 @@ def reduce_source_intensity(ray_list: PhotonList, ray_list.intensity[ir, inu] = ray_list.intensity[ir, inu] * wp.exp(-s[ir] * ray_list.kext[ir, inu] * grid.dust_density[ix, iy, iz]) - @wp.kernel - def set_ray_opacities(ray_list: PhotonList, - kext: wp.array2d(dtype=float), - albedo: wp.array2d(dtype=float), - irays: wp.array(dtype=int)): # pragma: no cover - - iray, inu = wp.tid() - ir = irays[iray] - - ray_list.kext[ir, inu] = kext[iray, inu] - ray_list.ray_albedo[ir, inu] = albedo[iray, inu] - @wp.kernel def set_ray_opacities_grid(ray_list: PhotonList, grid: GridStruct, diff --git a/pinballrt/model.py b/pinballrt/model.py index 40a7f6a..06505b8 100644 --- a/pinballrt/model.py +++ b/pinballrt/model.py @@ -3,7 +3,7 @@ from .sources import DiffuseSource, EnergySource from .grids import Grid -from .dust import load, Dust +from .dust import load from .gas import Gas from .camera import Camera from schwimmbad import SerialPool, MultiPool @@ -240,7 +240,8 @@ def scattering_mc(self, nphotons, wavelengths, device="cpu", return_timing=False [nphotons]*njobs, [njobs]*njobs, [wavelength]*njobs, - [i]*njobs,)) + [i]*njobs, + [self.camera_list[device].i_wp]*njobs)) total_scattering = [r[0] for r in result] time.sleep(0.1) t2 = time.time() @@ -337,15 +338,15 @@ def make_image(self, npix=100, pixel_size=None, channels=None, rest_frequency=No # First, run a scattering simulation to get the scattering phase function + for dev in self.camera_list: + self.camera_list[dev].set_orientation(incl, pa, distance) + self.grid_list[device].set_grid_opacities(nu) if include_dust: self.scattering_mc(nphotons, lam, device=device, set_grid_opacities=False) # Now set up the image proper. - for dev in self.camera_list: - self.camera_list[dev].set_orientation(incl, pa, distance) - physical_pixel_size = (pixel_size*distance).to(self.grid.distance_unit, equivalencies=u.dimensionless_angles()).value image = xr.Dataset( @@ -431,11 +432,11 @@ def thermal_mc_task(args): return grid.grid.energy.numpy(), iter_timing def scattering_mc_task(args): - grid, position, s, nphotons, njobs, wavelength, i = args + grid, position, s, nphotons, njobs, wavelength, i, camera_direction = args seed(s.generate_state(1)[0]) iter_timing = {} photon_list = grid.emit(int(nphotons / njobs), wavelength, scattering=True, timing=iter_timing) - grid.propagate_photons_scattering(photon_list, i, timing=iter_timing, position=position) + grid.propagate_photons_scattering(photon_list, i, camera_direction, timing=iter_timing, position=position) return grid.scattering, iter_timing diff --git a/pinballrt/photons.py b/pinballrt/photons.py index e43d8eb..26e1b12 100644 --- a/pinballrt/photons.py +++ b/pinballrt/photons.py @@ -17,6 +17,8 @@ class PhotonList: alpha: wp.array(dtype=float) kabs: wp.array(dtype=float) ksca: wp.array(dtype=float) + g: wp.array(dtype=float) + scattering_phase_function: wp.array(dtype=float) albedo: wp.array(dtype=float) kext: wp.array2d(dtype=float) ray_albedo: wp.array2d(dtype=float) diff --git a/pinballrt/sources.py b/pinballrt/sources.py index fc83859..16fd50f 100644 --- a/pinballrt/sources.py +++ b/pinballrt/sources.py @@ -19,7 +19,7 @@ from pinballrt.utils import EPSILON from .photons import PhotonList -from .utils import GridStruct +from .utils import GridStruct, random_direction from astropy.modeling import models import astropy.units as u import astropy.constants as const @@ -409,7 +409,7 @@ def emit(self, nphotons, distance_unit, wavelength="random", simulation="thermal photon_list.direction = wp.array(np.zeros((nphotons, 3)), dtype=wp.vec3) photon_list.direction_frame = wp.array(np.zeros((nphotons, 3)), dtype=wp.vec3) - wp.launch(kernel=self.grid.random_direction, + wp.launch(kernel=random_direction, dim=(nphotons,), inputs=[photon_list.direction, torch.arange(nphotons, dtype=torch.int32, device=wp.device_to_torch(wp.get_device())), np.random.randint(0, 100000)]) diff --git a/pinballrt/tests/data/UniformSphericalGrid_E2E_image.nc b/pinballrt/tests/data/UniformSphericalGrid_E2E_image.nc index e3bee29..8a659bf 100644 Binary files a/pinballrt/tests/data/UniformSphericalGrid_E2E_image.nc and b/pinballrt/tests/data/UniformSphericalGrid_E2E_image.nc differ diff --git a/pinballrt/tests/data/UniformSphericalGrid_E2E_mom0.nc b/pinballrt/tests/data/UniformSphericalGrid_E2E_mom0.nc index 2aec72c..7f104c2 100644 Binary files a/pinballrt/tests/data/UniformSphericalGrid_E2E_mom0.nc and b/pinballrt/tests/data/UniformSphericalGrid_E2E_mom0.nc differ diff --git a/pinballrt/tests/data/UniformSphericalGrid_E2E_scattering.npz b/pinballrt/tests/data/UniformSphericalGrid_E2E_scattering.npz index 29ddcff..faac0e1 100644 Binary files a/pinballrt/tests/data/UniformSphericalGrid_E2E_scattering.npz and b/pinballrt/tests/data/UniformSphericalGrid_E2E_scattering.npz differ diff --git a/pinballrt/tests/data/UniformSphericalGrid_E2E_temperature.npz b/pinballrt/tests/data/UniformSphericalGrid_E2E_temperature.npz index 1228a76..5cad17f 100644 Binary files a/pinballrt/tests/data/UniformSphericalGrid_E2E_temperature.npz and b/pinballrt/tests/data/UniformSphericalGrid_E2E_temperature.npz differ diff --git a/pinballrt/tests/data/diana.hg.dst b/pinballrt/tests/data/diana.hg.dst new file mode 100644 index 0000000..9d2de08 Binary files /dev/null and b/pinballrt/tests/data/diana.hg.dst differ diff --git a/pinballrt/tests/test_E2E.py b/pinballrt/tests/test_E2E.py index b11c7c5..e8b398c 100644 --- a/pinballrt/tests/test_E2E.py +++ b/pinballrt/tests/test_E2E.py @@ -13,20 +13,20 @@ import pytest test_data = [ - (UniformCartesianGrid, {"ncells":9, "dx":2.0*u.au}, 98.0), - (UniformSphericalGrid, {"ncells":9, "dr":2.0*u.au}, 93.0), - (LogUniformSphericalGrid, {"ncells":9, "rmin":0.1*u.au, "rmax":20.0*u.au}, 73.0), + (UniformCartesianGrid, {"ncells":9, "dx":2.0*u.au}, "iso", 98.0), + (UniformSphericalGrid, {"ncells":9, "dr":2.0*u.au}, "hg", 93.0), + (LogUniformSphericalGrid, {"ncells":9, "rmin":0.1*u.au, "rmax":20.0*u.au}, "iso", 73.0), ] -@pytest.mark.parametrize("grid_class,grid_kwargs,percentile", test_data) -def test_E2E(grid_class, grid_kwargs, percentile, return_vals=False): +@pytest.mark.parametrize("grid_class,grid_kwargs,dust,percentile", test_data) +def test_E2E(grid_class, grid_kwargs, dust, percentile, return_vals=False): """ Test the end-to-end functionality of the UniformCartesianGrid model running all the way through. """ # Set up the dust. - d = os.path.join(os.path.dirname(__file__), "data/diana.iso.dst") + d = os.path.join(os.path.dirname(__file__), f"data/diana.{dust}.dst") # Set up the grid. model = Model(grid=grid_class, grid_kwargs=grid_kwargs) @@ -111,7 +111,7 @@ def update_test(test, grid_class): if not found: raise ValueError(f"Grid class {grid_class} not found in test data. Please add it to the test_data list in test_E2E.py.") - temperature, scattering, image, mom0 = test(grid_class, data[1], data[2], return_vals=True) + temperature, scattering, image, mom0 = test(grid_class, data[1], data[2], data[3], return_vals=True) np.savez(os.path.join(os.path.dirname(__file__), f"data/{grid_class.__name__}_E2E_temperature.npz"), temperature=temperature) np.savez(os.path.join(os.path.dirname(__file__), f"data/{grid_class.__name__}_E2E_scattering.npz"), scattering=scattering) diff --git a/pinballrt/tests/test_dust_creation.py b/pinballrt/tests/test_dust_creation.py index 0a5f8b9..e8d0f7a 100644 --- a/pinballrt/tests/test_dust_creation.py +++ b/pinballrt/tests/test_dust_creation.py @@ -1,4 +1,4 @@ -from pinballrt.dust import Dust, load, suggest_opacity_sampling +from pinballrt.dust import IsotropicDust, HenyeyGreensteinDust, GeneralDust, load, suggest_opacity_sampling import numpy as np import astropy.units as u import pytest @@ -14,25 +14,31 @@ def test_Dust(): p, amax = np.meshgrid(data["p"], data["amax"], indexing="ij") - d = Dust(lam=data["lam"]*u.cm, - kabs=data["kabs"]*u.cm**2/u.g, - ksca=data["ksca"]*u.cm**2/u.g, - amax=amax*u.cm, - p=p) + d = IsotropicDust(lam=data["lam"]*u.cm, + kabs=data["kabs"]*u.cm**2/u.g, + ksca=data["ksca"]*u.cm**2/u.g, + amax=amax*u.cm, + p=p) assert d.kmean.value == 4574.907442551958 d.save("amax.dst") @pytest.mark.parametrize( - "dims", + "dust_type,dims", [ - pytest.param((), id="None"), - pytest.param(("amax","abundances"), id="amax,abundances"), - pytest.param(("p","amax","abundances"), id="p,amax,abundances"), + pytest.param(IsotropicDust, (), id="IsotropicDust/None"), + pytest.param(IsotropicDust, ("amax","abundances"), id="IsotropicDust/amax,abundances"), + pytest.param(IsotropicDust, ("p","amax","abundances"), id="IsotropicDust/p,amax,abundances"), + pytest.param(HenyeyGreensteinDust, (), id="HenyeyGreensteinDust/None"), + pytest.param(HenyeyGreensteinDust, ("p","abundances"), id="HenyeyGreensteinDust/p,abundances"), + pytest.param(HenyeyGreensteinDust, ("p","amax","abundances"), id="HenyeyGreensteinDust/p,amax,abundances"), + pytest.param(GeneralDust, (), id="GeneralDust/None"), + pytest.param(GeneralDust, ("p","abundances"), id="GeneralDust/p,abundances"), + pytest.param(GeneralDust, ("p","amax","abundances"), id="GeneralDust/p,amax,abundances"), ] ) -def test_learning(dims): +def test_learning(dust_type, dims): """ Test the learn_random_nu method of the Dust class. """ @@ -92,17 +98,37 @@ def test_learning(dims): kappa_scat = 0.5 * (wavelengths.to(u.micron).value/100.0)**power_law_index * u.cm**2 / u.g + silicate_feature + water_feature # Create the Dust object. - d = Dust(lam=wavelengths[0,:], - amax=amax[:,0] if "amax" in dims else None, - p=p[:,0] if "p" in dims else None, - abundances=(silicate_fraction[:,0], water_fraction[:,0]) if "abundances" in dims else (), - kabs=kappa_abs, - ksca=kappa_scat, - ntemperatures=10) + dust_kwargs = {"lam":wavelengths[0,:], + "amax":amax[:,0] if "amax" in dims else None, + "p":p[:,0] if "p" in dims else None, + "abundances":(silicate_fraction[:,0], water_fraction[:,0]) if "abundances" in dims else (), + "kabs":kappa_abs, + "ksca":kappa_scat, + "ntemperatures":10} + + if dust_type == HenyeyGreensteinDust: + g = np.tanh(p - np.log10(wavelengths.to(u.micron).value)) + dust_kwargs["g"] = g + elif dust_type == GeneralDust: + g = np.repeat(np.expand_dims(np.tanh(p - np.log10(wavelengths.to(u.micron).value)), axis=-1), 5, axis=-1) + theta = np.tile(np.expand_dims(np.linspace(0, 180., 5), axis=(0,1)), (10 if len(dims) > 0 else 1, 10, 1)) * u.deg + scattering_phase_function = (1 - g**2) / (4 * np.pi * (1 + g**2 - 2*g*np.cos(theta.to(u.rad).value))**(3/2)) + + dust_kwargs["scattering_phase_function"] = scattering_phase_function + dust_kwargs["theta"] = theta[0,0,:] + + d = dust_type(**dust_kwargs) # Test the learn_random_nu method. - for model in ["kabs","ksca","pmo","random_nu"]: + models = ["kabs","ksca","pmo","random_nu"] + if dust_type == HenyeyGreensteinDust: + models.append("g") + if dust_type == GeneralDust: + models.append("scattering_phase_function") + models.append("random_direction") + + for model in models: print('*****************************') print(f'{model}') print('*****************************') @@ -143,10 +169,10 @@ def test_dust_pickle(): p, amax = np.meshgrid(data["p"], data["amax"], indexing="ij") - d = Dust(lam=data["lam"]*u.cm, - kabs=data["kabs"]*u.cm**2/u.g, - ksca=data["ksca"]*u.cm**2/u.g, - amax=amax*u.cm, - p=p) + d = IsotropicDust(lam=data["lam"]*u.cm, + kabs=data["kabs"]*u.cm**2/u.g, + ksca=data["ksca"]*u.cm**2/u.g, + amax=amax*u.cm, + p=p) result = dill.loads(dill.dumps(d)) diff --git a/pinballrt/utils.py b/pinballrt/utils.py index a739e69..fb048f8 100644 --- a/pinballrt/utils.py +++ b/pinballrt/utils.py @@ -69,6 +69,23 @@ def calculate_Qvalue(array1, array2, percentile=99.0, clip=None): return Q +@wp.kernel +def random_direction(direction: wp.array(dtype=wp.vec3), + iphotons: wp.array(dtype=int), + seed: int): # pragma: no cover + i = wp.tid() + ip = iphotons[i] + + rng = wp.rand_init(seed, i) + + cost = -1. + 2.*wp.randf(rng) + sint = wp.sqrt(1.-cost**2.) + phi = 2.*np.pi*wp.randf(rng) + + direction[ip][0] = sint*np.cos(phi) + direction[ip][1] = sint*np.sin(phi) + direction[ip][2] = cost + @wp.struct class GridStruct: w1: wp.array(dtype=float) @@ -100,6 +117,7 @@ class GridStruct: kabs: wp.array4d(dtype=float) ksca: wp.array4d(dtype=float) + g: wp.array4d(dtype=float) velocity: wp.array4d(dtype=float) microturbulence: wp.array3d(dtype=float)