dust-compendium 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dust_compendium-0.1.0.dist-info/METADATA +108 -0
- dust_compendium-0.1.0.dist-info/RECORD +30 -0
- dust_compendium-0.1.0.dist-info/WHEEL +4 -0
- dust_compendium-0.1.0.dist-info/entry_points.txt +2 -0
- dust_compendium-0.1.0.dist-info/licenses/LICENSE +21 -0
- dustcompendium/__init__.py +14 -0
- dustcompendium/campaign.py +259 -0
- dustcompendium/cli.py +480 -0
- dustcompendium/config.py +239 -0
- dustcompendium/dust/__init__.py +17 -0
- dustcompendium/dust/data/gordon1997.csv +26 -0
- dustcompendium/dust/ferrara.py +151 -0
- dustcompendium/dust/properties.py +80 -0
- dustcompendium/galaxy.py +151 -0
- dustcompendium/geometry/__init__.py +82 -0
- dustcompendium/geometry/base.py +160 -0
- dustcompendium/geometry/disk.py +175 -0
- dustcompendium/geometry/spheroid.py +395 -0
- dustcompendium/grid.py +218 -0
- dustcompendium/hyperion_model.py +151 -0
- dustcompendium/model.py +246 -0
- dustcompendium/plots.py +325 -0
- dustcompendium/postprocess.py +316 -0
- dustcompendium/runner/__init__.py +44 -0
- dustcompendium/runner/base.py +122 -0
- dustcompendium/runner/local.py +60 -0
- dustcompendium/runner/slurm.py +181 -0
- dustcompendium/runner/solver.py +104 -0
- dustcompendium/tabulation.py +282 -0
- dustcompendium/validation.py +162 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dust-compendium
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dust attenuation tables for simple galactic geometries, computed with Hyperion
|
|
5
|
+
Project-URL: Homepage, https://github.com/galacticusorg/dust-compendium
|
|
6
|
+
Project-URL: Documentation, https://dust-compendium.readthedocs.io
|
|
7
|
+
Project-URL: Issues, https://github.com/galacticusorg/dust-compendium/issues
|
|
8
|
+
Author-email: Andrew Benson <abenson@carnegiescience.edu>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: astronomy,dust,galacticus,galaxies,radiative transfer
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: h5py>=3.8
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: pyyaml>=6.0
|
|
21
|
+
Requires-Dist: scipy>=1.10
|
|
22
|
+
Requires-Dist: typer>=0.12
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pre-commit>=3.5; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff==0.16.7; extra == 'dev'
|
|
28
|
+
Provides-Extra: docs
|
|
29
|
+
Requires-Dist: myst-parser>=2.0; extra == 'docs'
|
|
30
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
31
|
+
Requires-Dist: sphinx-book-theme; extra == 'docs'
|
|
32
|
+
Requires-Dist: sphinx>=7.0; extra == 'docs'
|
|
33
|
+
Provides-Extra: plots
|
|
34
|
+
Requires-Dist: matplotlib>=3.7; extra == 'plots'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# Dust Compendium
|
|
38
|
+
|
|
39
|
+
[](https://github.com/galacticusorg/dust-compendium/actions/workflows/ci.yml)
|
|
40
|
+
[](https://dust-compendium.readthedocs.io)
|
|
41
|
+
|
|
42
|
+
Dust attenuation tables for simple galactic geometries, computed by running the
|
|
43
|
+
[Hyperion](http://www.hyperion-rt.org) Monte Carlo radiative transfer code over
|
|
44
|
+
a grid of galaxy models.
|
|
45
|
+
|
|
46
|
+
A tabulation gives the fraction of starlight escaping a galaxy as a function of
|
|
47
|
+
wavelength, inclination, the optical depth of each dusty component, and the
|
|
48
|
+
sizes of the components themselves. [Galacticus](https://github.com/galacticusorg/galacticus)
|
|
49
|
+
reads them through its `dustAttenuationAtlasCompendium` class. The method is
|
|
50
|
+
described in [Benson (2018)](https://ui.adsabs.harvard.edu/abs/2018RNAAS...2..188B).
|
|
51
|
+
|
|
52
|
+
Full documentation is at [dust-compendium.readthedocs.io](https://dust-compendium.readthedocs.io).
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install dust-compendium
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That gives the library and the command line tool. Computing *new* tabulations
|
|
61
|
+
additionally needs Hyperion, in two parts, neither of which is a dependency of
|
|
62
|
+
this package:
|
|
63
|
+
|
|
64
|
+
- its **Python front-end**, which has to come from source for now — the current
|
|
65
|
+
PyPI release calls `np.string_` when writing a model, which NumPy 2 removed;
|
|
66
|
+
- its **Fortran solvers**, which `pip` does not install at all.
|
|
67
|
+
|
|
68
|
+
See [the installation notes](https://dust-compendium.readthedocs.io/en/latest/installation.html).
|
|
69
|
+
|
|
70
|
+
## Using it
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dust-compendium validate configs/compendium-d03-rv3.1.yaml # what would this run?
|
|
74
|
+
dust-compendium build configs/compendium-d03-rv3.1.yaml # write the model inputs
|
|
75
|
+
dust-compendium run configs/compendium-d03-rv3.1.yaml # solve them, here or on Slurm
|
|
76
|
+
dust-compendium collect configs/compendium-d03-rv3.1.yaml # assemble the tabulation
|
|
77
|
+
dust-compendium plot attenuations.hdf5 -q curve -c opticalDepth
|
|
78
|
+
dust-compendium compare attenuations.hdf5 --atlas /path/to/atlas.hdf5
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A campaign is described by a YAML file. Any value written as a range becomes an
|
|
82
|
+
axis of the tabulation; a galaxy is a set of named components, each of which may
|
|
83
|
+
carry stars, dust, both or neither. Each step reads what the one before it left
|
|
84
|
+
on disk, so any of them can be interrupted and resumed.
|
|
85
|
+
|
|
86
|
+
`configs/` holds the configuration for the published Draine R_V=3.1 tabulation,
|
|
87
|
+
one adding dust to the spheroid, and one matched to Ferrara et al. (1999) for
|
|
88
|
+
validation.
|
|
89
|
+
|
|
90
|
+
## Validation
|
|
91
|
+
|
|
92
|
+
Reproducing Ferrara et al. (1999) with matched grains, geometry and grids gives
|
|
93
|
+
a median absolute difference from their published atlas of **0.0034** in disk
|
|
94
|
+
transmission, with 99.9% of entries within 0.02 — as close as the original
|
|
95
|
+
scripts got, and near the 0.005 floor set by the atlas being published to two
|
|
96
|
+
decimal places.
|
|
97
|
+
|
|
98
|
+
## Provenance
|
|
99
|
+
|
|
100
|
+
This replaces the Perl and Python 2 scripts written for the 2018 paper, which
|
|
101
|
+
are preserved verbatim in the first commit of this repository. Rewriting them
|
|
102
|
+
turned up several defects, two of which reached published data; those are
|
|
103
|
+
described under
|
|
104
|
+
[Published datasets](https://dust-compendium.readthedocs.io/en/latest/datasets.html).
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
dustcompendium/__init__.py,sha256=7rA1LA0SktQZR215XSlteRjjNxDvzPsWhxRg2iOBSqA,479
|
|
2
|
+
dustcompendium/campaign.py,sha256=UJoTc_t6UmDrc4lKVbQuVbvuMbopXTctpPRCuu6bv-c,10878
|
|
3
|
+
dustcompendium/cli.py,sha256=wB0x8cJ2RMU5DYWq52X_v35Fg55CI4xQ4o6X3cLR-C4,18736
|
|
4
|
+
dustcompendium/config.py,sha256=ds6u3hVxrd5IBMKqRJzbUQCNqCPwU1Sg9kfxUE7DMXY,9433
|
|
5
|
+
dustcompendium/galaxy.py,sha256=af_02zpCKdQNxcIQ6o0CgaIZGTAhTaCKEx1VjLScaEk,5672
|
|
6
|
+
dustcompendium/grid.py,sha256=skfpvX2aEp6wuhsaK0BPyIUfiKyc2LdmaA8cxH4FoYo,9302
|
|
7
|
+
dustcompendium/hyperion_model.py,sha256=gKc_-s_pkX6eXRFOnnpz4lWp3SVrkewczYUpAh0O_z8,5418
|
|
8
|
+
dustcompendium/model.py,sha256=0DyM2cB0YoNNrVMrB_GgQ4i44KVqLYTHMp7eWpyOU54,9969
|
|
9
|
+
dustcompendium/plots.py,sha256=lGG7Ms7gBzGttr3BI3WwWsbP-kxnYqJrRXc3r7YLai4,12710
|
|
10
|
+
dustcompendium/postprocess.py,sha256=VOW7lNt9V_Uvcs97i7oLJNdDdS8-Si8PLdmaCO0xqzk,12469
|
|
11
|
+
dustcompendium/tabulation.py,sha256=YlyWFKTFvY5IUdfY-RvfZA6jyjo4yZ8wrcC10hS3Cis,12159
|
|
12
|
+
dustcompendium/validation.py,sha256=tlo7ZSgM3gVLCTEFtlRcpuYTSE6XYuTpUUcB9lvIViA,5822
|
|
13
|
+
dustcompendium/dust/__init__.py,sha256=yWcio7isC6u3AavSXuRPKy1Gw6LLLKtn3FXujasvYYk,316
|
|
14
|
+
dustcompendium/dust/ferrara.py,sha256=KkNPgTT8hf38MjjBpNLkqQQLhyBZzWWQHAkABLnSSDw,5286
|
|
15
|
+
dustcompendium/dust/properties.py,sha256=RNPPbpTp7J77Ja1XWmPzNYaS0HnpV5jlgLYQXCAJYak,2707
|
|
16
|
+
dustcompendium/dust/data/gordon1997.csv,sha256=v7r1Sk7kzfsxx6hiR-8IYOW4gY6IJCLENMR47DC7L5U,1325
|
|
17
|
+
dustcompendium/geometry/__init__.py,sha256=VjSuSP2OdoFKMaWnkIUMKAq5LOO8u2Sx4bzCft4SFTU,2342
|
|
18
|
+
dustcompendium/geometry/base.py,sha256=TkSR68lnX9jgG1od6Z0YQ11BDb-1FnqVPyCTPqckDpU,5867
|
|
19
|
+
dustcompendium/geometry/disk.py,sha256=SXsgKZtmmcXL6mQxMtov-gZmqThnoL_vc5g45uR8NHw,6150
|
|
20
|
+
dustcompendium/geometry/spheroid.py,sha256=emyUi8n_PE0-Wq-Er6uMPVBfd9S1d5oVhtUVa0TLj4E,17987
|
|
21
|
+
dustcompendium/runner/__init__.py,sha256=lJ4INQBG_XywZZbe6asJrPRppZEPMR05cU3mLtVYG58,1132
|
|
22
|
+
dustcompendium/runner/base.py,sha256=NJUYjAR83hBxNZSlsT_RLHvst5IyvsuAcAFs8P0KxSc,4037
|
|
23
|
+
dustcompendium/runner/local.py,sha256=gXXUzcWivrmCROuD75py-I86FlLH5-l2O4X-h4M9DBA,2104
|
|
24
|
+
dustcompendium/runner/slurm.py,sha256=GLieuIVAgD92VChxTmE7ERRIs7PEgbp_DiPVuKERG08,7155
|
|
25
|
+
dustcompendium/runner/solver.py,sha256=AnGX7widY1v3YpxCWIHR503pN5gIod8YVGNPR7YaCKM,3695
|
|
26
|
+
dust_compendium-0.1.0.dist-info/METADATA,sha256=AjrtzjNWX2GmWGpwsS2ebRQnlc1mrmLqbPIVqEgSQdM,4735
|
|
27
|
+
dust_compendium-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
28
|
+
dust_compendium-0.1.0.dist-info/entry_points.txt,sha256=I1I2b9Ss3aSvG_jZyOiXnNK7Gh5ERwoqDKLCVDUj960,59
|
|
29
|
+
dust_compendium-0.1.0.dist-info/licenses/LICENSE,sha256=Dg5aovcCb-1jZaq8wyx5dK6xzuTA-RmIzbhF4-cBYy0,1075
|
|
30
|
+
dust_compendium-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018-2026 Andrew Benson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Dust attenuation tables for simple galactic geometries.
|
|
2
|
+
|
|
3
|
+
Computes the fraction of starlight escaping a galaxy as a function of
|
|
4
|
+
wavelength, inclination, dust optical depth and component size, by running the
|
|
5
|
+
Hyperion Monte Carlo radiative transfer code over a grid of simple galactic
|
|
6
|
+
geometries, and tabulates the result.
|
|
7
|
+
|
|
8
|
+
The method is described in `Benson (2018)
|
|
9
|
+
<https://ui.adsabs.harvard.edu/abs/2018RNAAS...2..188B>`_.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
r"""Expanding a configuration into the individual models that have to be run.
|
|
2
|
+
|
|
3
|
+
A campaign tabulates attenuation over a grid of parameters. Every parameter
|
|
4
|
+
written as a range in the configuration becomes an *axis*: the optical depth of
|
|
5
|
+
each dust-bearing component, and any geometric scale that varies. One model is
|
|
6
|
+
run for each point on that grid and for each component with stars, since the
|
|
7
|
+
attenuation of each is tabulated separately.
|
|
8
|
+
|
|
9
|
+
The published compendium is 60 optical depths by 51 spheroid sizes by two
|
|
10
|
+
emitting components, which is 6,120 models -- why this wants a cluster.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from itertools import product
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from numpy.typing import NDArray
|
|
19
|
+
|
|
20
|
+
from .config import CampaignConfig, ProfileConfig, values_of
|
|
21
|
+
from .galaxy import Component, Galaxy
|
|
22
|
+
from .model import ModelSpec
|
|
23
|
+
|
|
24
|
+
__all__ = ["Axis", "Campaign", "Run"]
|
|
25
|
+
|
|
26
|
+
#: Geometric parameters which may vary, and so become axes.
|
|
27
|
+
GEOMETRIC = ("scale_radial", "scale_height")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Axis:
|
|
32
|
+
"""One tabulated parameter, and the values it takes."""
|
|
33
|
+
|
|
34
|
+
kind: str
|
|
35
|
+
"""``opticalDepth``, ``scaleRadial`` or ``scaleHeight``."""
|
|
36
|
+
component: str
|
|
37
|
+
"""Which component it belongs to."""
|
|
38
|
+
roles: frozenset[str]
|
|
39
|
+
"""Which of ``stellar`` and ``dust`` this axis sets.
|
|
40
|
+
|
|
41
|
+
A component's stars and its dust may have different scale lengths -- the
|
|
42
|
+
original already gave the disk separate stellar and dust scale heights -- so
|
|
43
|
+
the two are separate parameters. When they are configured over the same
|
|
44
|
+
values they are one axis, driving both, rather than a square grid of a scale
|
|
45
|
+
against itself.
|
|
46
|
+
"""
|
|
47
|
+
values: NDArray[np.float64]
|
|
48
|
+
qualified: bool = False
|
|
49
|
+
"""Whether the name needs a role to tell it from another axis of the same parameter."""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def name(self) -> str:
|
|
53
|
+
"""A stable name, used for labels and for output dataset names."""
|
|
54
|
+
if self.qualified:
|
|
55
|
+
return f"{self.kind}:{self.component}:{sorted(self.roles)[0]}"
|
|
56
|
+
return f"{self.kind}:{self.component}"
|
|
57
|
+
|
|
58
|
+
def affects(self, emitter: str) -> bool:
|
|
59
|
+
"""Whether this axis can change what the given emitter's light looks like.
|
|
60
|
+
|
|
61
|
+
Attenuation depends on every dust distribution, but only on the stellar
|
|
62
|
+
distribution of the component being observed. So a geometric axis
|
|
63
|
+
belonging to another component's *stars* changes nothing, and tabulating
|
|
64
|
+
against it would repeat the same model. The original arranged this by
|
|
65
|
+
hand, pinning the spheroid scale to a single value when the disk was
|
|
66
|
+
emitting, which is why ``attenuationDisk`` carries no spheroid axis.
|
|
67
|
+
"""
|
|
68
|
+
return "dust" in self.roles or self.component == emitter
|
|
69
|
+
|
|
70
|
+
def __len__(self) -> int:
|
|
71
|
+
return int(self.values.size)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class Run:
|
|
76
|
+
"""One model: a point on the parameter grid, seen from one emitting component."""
|
|
77
|
+
|
|
78
|
+
emitter: str
|
|
79
|
+
indices: tuple[int, ...]
|
|
80
|
+
"""Index into each of :attr:`Campaign.axes`, in order."""
|
|
81
|
+
spec: ModelSpec
|
|
82
|
+
label: str
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def file_stem(self) -> str:
|
|
86
|
+
"""A file name stem, with the characters a path would rather not carry."""
|
|
87
|
+
return self.label.replace(":", "_").replace("/", "_").replace(" ", "_")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Campaign:
|
|
91
|
+
"""The set of models a configuration describes.
|
|
92
|
+
|
|
93
|
+
Parameters
|
|
94
|
+
----------
|
|
95
|
+
config
|
|
96
|
+
The parsed configuration.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, config: CampaignConfig) -> None:
|
|
100
|
+
self.config = config
|
|
101
|
+
self.axes = tuple(self._axes())
|
|
102
|
+
self.wavelengths = values_of(config.tabulation.wavelengths)
|
|
103
|
+
self.inclinations = values_of(config.tabulation.inclinations)
|
|
104
|
+
|
|
105
|
+
def _axes(self) -> Iterator[Axis]:
|
|
106
|
+
for name, component in self.config.geometry.components.items():
|
|
107
|
+
roles = (("stellar", component.stellar), ("dust", component.dust))
|
|
108
|
+
for role, profile in roles:
|
|
109
|
+
if profile is None or role != "dust":
|
|
110
|
+
continue
|
|
111
|
+
if profile.optical_depth is not None:
|
|
112
|
+
yield Axis("opticalDepth", name, frozenset({role}), values_of(profile.optical_depth))
|
|
113
|
+
for parameter in GEOMETRIC:
|
|
114
|
+
kind = parameter.replace("_r", "R").replace("_h", "H")
|
|
115
|
+
# Group the roles which vary this parameter over identical
|
|
116
|
+
# values; each distinct set of values is one axis.
|
|
117
|
+
groups: list[tuple[NDArray[np.float64], set[str]]] = []
|
|
118
|
+
for role, profile in roles:
|
|
119
|
+
if profile is None or getattr(profile, parameter, None) is None:
|
|
120
|
+
continue
|
|
121
|
+
values = values_of(getattr(profile, parameter))
|
|
122
|
+
if values.size <= 1:
|
|
123
|
+
continue
|
|
124
|
+
for existing, members in groups:
|
|
125
|
+
if existing.shape == values.shape and np.array_equal(existing, values):
|
|
126
|
+
members.add(role)
|
|
127
|
+
break
|
|
128
|
+
else:
|
|
129
|
+
groups.append((values, {role}))
|
|
130
|
+
for values, members in groups:
|
|
131
|
+
yield Axis(kind, name, frozenset(members), values, qualified=len(groups) > 1)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def emitters(self) -> tuple[str, ...]:
|
|
135
|
+
"""Components with stars, each of which is tabulated separately."""
|
|
136
|
+
return tuple(
|
|
137
|
+
name
|
|
138
|
+
for name, component in self.config.geometry.components.items()
|
|
139
|
+
if component.stellar is not None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def axes_for(self, emitter: str) -> tuple[Axis, ...]:
|
|
143
|
+
"""The axes the given emitter is actually tabulated against."""
|
|
144
|
+
return tuple(axis for axis in self.axes if axis.affects(emitter))
|
|
145
|
+
|
|
146
|
+
def shape_for(self, emitter: str) -> tuple[int, ...]:
|
|
147
|
+
"""Length of each axis the given emitter is tabulated against."""
|
|
148
|
+
return tuple(len(axis) for axis in self.axes_for(emitter))
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def shape(self) -> tuple[int, ...]:
|
|
152
|
+
"""Length of each axis, in order."""
|
|
153
|
+
return tuple(len(axis) for axis in self.axes)
|
|
154
|
+
|
|
155
|
+
def __len__(self) -> int:
|
|
156
|
+
"""How many models the campaign contains."""
|
|
157
|
+
return sum(int(np.prod(self.shape_for(emitter), dtype=int)) for emitter in self.emitters)
|
|
158
|
+
|
|
159
|
+
def _chosen(
|
|
160
|
+
self, indices: Sequence[int], axes: Sequence[Axis] | None = None
|
|
161
|
+
) -> dict[tuple[str, str, str], float]:
|
|
162
|
+
"""The value each axis takes at a point, keyed by component, role and parameter."""
|
|
163
|
+
chosen = {}
|
|
164
|
+
for axis, index in zip(axes if axes is not None else self.axes, indices, strict=True):
|
|
165
|
+
parameter = {"scaleRadial": "scale_radial", "scaleHeight": "scale_height"}.get(
|
|
166
|
+
axis.kind, "optical_depth"
|
|
167
|
+
)
|
|
168
|
+
for role in axis.roles:
|
|
169
|
+
chosen[(axis.component, role, parameter)] = float(axis.values[index])
|
|
170
|
+
return chosen
|
|
171
|
+
|
|
172
|
+
def galaxy(
|
|
173
|
+
self, indices: Sequence[int], axes: Sequence[Axis] | None = None, emitter: str | None = None
|
|
174
|
+
) -> Galaxy:
|
|
175
|
+
"""The galaxy at a point on the parameter grid, as one emitter sees it.
|
|
176
|
+
|
|
177
|
+
A component which neither emits for this run nor carries dust cannot
|
|
178
|
+
change the answer, and is left out. That is not merely an economy: a
|
|
179
|
+
component included needlessly still sizes the grid, and a large spheroid
|
|
180
|
+
sizing the grid for a disk-only model is what leaves the disk
|
|
181
|
+
unresolved. The original arranged the same thing by hand, pinning the
|
|
182
|
+
spheroid scale to zero whenever the disk was emitting.
|
|
183
|
+
"""
|
|
184
|
+
chosen = self._chosen(indices, axes)
|
|
185
|
+
|
|
186
|
+
def build(name: str, role: str, profile: ProfileConfig | None):
|
|
187
|
+
if profile is None:
|
|
188
|
+
return None
|
|
189
|
+
overrides = {
|
|
190
|
+
parameter: value
|
|
191
|
+
for (component, kind, parameter), value in chosen.items()
|
|
192
|
+
if component == name and kind == role and parameter in GEOMETRIC
|
|
193
|
+
}
|
|
194
|
+
return profile.build(**overrides)
|
|
195
|
+
|
|
196
|
+
components = []
|
|
197
|
+
for name, component in self.config.geometry.components.items():
|
|
198
|
+
emits = emitter is None or name == emitter
|
|
199
|
+
if not emits and component.dust is None:
|
|
200
|
+
continue
|
|
201
|
+
components.append(
|
|
202
|
+
Component(
|
|
203
|
+
name,
|
|
204
|
+
stellar=build(name, "stellar", component.stellar) if emits else None,
|
|
205
|
+
dust=build(name, "dust", component.dust),
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
return Galaxy(components)
|
|
209
|
+
|
|
210
|
+
def optical_depths(self, indices: Sequence[int], axes: Sequence[Axis] | None = None) -> dict[str, float]:
|
|
211
|
+
"""The optical depth of each dust-bearing component at a point."""
|
|
212
|
+
chosen = self._chosen(indices, axes)
|
|
213
|
+
return {
|
|
214
|
+
component: value
|
|
215
|
+
for (component, _role, parameter), value in chosen.items()
|
|
216
|
+
if parameter == "optical_depth"
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
def runs(self) -> Iterator[Run]:
|
|
220
|
+
"""Every model in the campaign, in a stable order.
|
|
221
|
+
|
|
222
|
+
The seed is decremented per model, as the original did, so that each gets
|
|
223
|
+
its own realization rather than repeating one.
|
|
224
|
+
"""
|
|
225
|
+
tabulation = self.config.tabulation
|
|
226
|
+
seed = tabulation.seed
|
|
227
|
+
for emitter in self.emitters:
|
|
228
|
+
axes = self.axes_for(emitter)
|
|
229
|
+
for indices in product(*(range(len(axis)) for axis in axes)):
|
|
230
|
+
seed -= 1
|
|
231
|
+
suffix = "_".join(str(index) for index in indices)
|
|
232
|
+
yield Run(
|
|
233
|
+
emitter=emitter,
|
|
234
|
+
indices=indices,
|
|
235
|
+
label=f"{self.config.label}_{emitter}" + (f"_{suffix}" if suffix else ""),
|
|
236
|
+
spec=ModelSpec(
|
|
237
|
+
galaxy=self.galaxy(indices, axes, emitter),
|
|
238
|
+
emitter=emitter,
|
|
239
|
+
optical_depths=self.optical_depths(indices, axes),
|
|
240
|
+
wavelengths=self.wavelengths,
|
|
241
|
+
inclinations=self.inclinations,
|
|
242
|
+
cut_off=self.config.geometry.cut_off,
|
|
243
|
+
photons=tabulation.photons,
|
|
244
|
+
seed=seed,
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def grid_options(self) -> Mapping[str, object]:
|
|
249
|
+
"""Grid options from the configuration, to pass when building a model."""
|
|
250
|
+
geometry = self.config.geometry
|
|
251
|
+
return {
|
|
252
|
+
"spacing": geometry.spacing,
|
|
253
|
+
"radial_cells": geometry.radial_cells,
|
|
254
|
+
"vertical_cells": geometry.vertical_cells,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
def __repr__(self) -> str:
|
|
258
|
+
axes = ", ".join(f"{axis.name}[{len(axis)}]" for axis in self.axes)
|
|
259
|
+
return f"Campaign({self.config.label!r}, {len(self)} models, axes: {axes})"
|