skyscapes 0.0.1__tar.gz
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.
- skyscapes-0.0.1/.gitignore +166 -0
- skyscapes-0.0.1/.pre-commit-config.yaml +20 -0
- skyscapes-0.0.1/.readthedocs.yaml +20 -0
- skyscapes-0.0.1/CHANGELOG.md +8 -0
- skyscapes-0.0.1/LICENSE +21 -0
- skyscapes-0.0.1/PKG-INFO +62 -0
- skyscapes-0.0.1/README.md +5 -0
- skyscapes-0.0.1/pyproject.toml +59 -0
- skyscapes-0.0.1/src/skyscapes/__init__.py +23 -0
- skyscapes-0.0.1/src/skyscapes/_version.py +24 -0
- skyscapes-0.0.1/src/skyscapes/disk.py +59 -0
- skyscapes-0.0.1/src/skyscapes/loaders.py +400 -0
- skyscapes-0.0.1/src/skyscapes/planet.py +78 -0
- skyscapes-0.0.1/src/skyscapes/star.py +128 -0
- skyscapes-0.0.1/src/skyscapes/system.py +27 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# poetry
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
102
|
+
#poetry.lock
|
|
103
|
+
|
|
104
|
+
# pdm
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
106
|
+
#pdm.lock
|
|
107
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
108
|
+
# in version control.
|
|
109
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
110
|
+
.pdm.toml
|
|
111
|
+
|
|
112
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
113
|
+
__pypackages__/
|
|
114
|
+
|
|
115
|
+
# Celery stuff
|
|
116
|
+
celerybeat-schedule
|
|
117
|
+
celerybeat.pid
|
|
118
|
+
|
|
119
|
+
# SageMath parsed files
|
|
120
|
+
*.sage.py
|
|
121
|
+
|
|
122
|
+
# Environments
|
|
123
|
+
.env
|
|
124
|
+
.venv
|
|
125
|
+
env/
|
|
126
|
+
venv/
|
|
127
|
+
ENV/
|
|
128
|
+
env.bak/
|
|
129
|
+
venv.bak/
|
|
130
|
+
|
|
131
|
+
# Spyder project settings
|
|
132
|
+
.spyderproject
|
|
133
|
+
.spyproject
|
|
134
|
+
|
|
135
|
+
# Rope project settings
|
|
136
|
+
.ropeproject
|
|
137
|
+
|
|
138
|
+
# mkdocs documentation
|
|
139
|
+
/site
|
|
140
|
+
|
|
141
|
+
# mypy
|
|
142
|
+
.mypy_cache/
|
|
143
|
+
.dmypy.json
|
|
144
|
+
dmypy.json
|
|
145
|
+
|
|
146
|
+
# Pyre type checker
|
|
147
|
+
.pyre/
|
|
148
|
+
|
|
149
|
+
# pytype static type analyzer
|
|
150
|
+
.pytype/
|
|
151
|
+
|
|
152
|
+
# Cython debug symbols
|
|
153
|
+
cython_debug/
|
|
154
|
+
|
|
155
|
+
# PyCharm
|
|
156
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
157
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
158
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
159
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
160
|
+
#.idea/
|
|
161
|
+
*_version.py
|
|
162
|
+
.DS_Store
|
|
163
|
+
input/
|
|
164
|
+
output/
|
|
165
|
+
scripts/
|
|
166
|
+
*.png
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: "v6.0.0"
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: name-tests-test
|
|
7
|
+
args: [--pytest-test-first]
|
|
8
|
+
- id: end-of-file-fixer
|
|
9
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
10
|
+
rev: v0.15.8
|
|
11
|
+
hooks:
|
|
12
|
+
- id: ruff-check
|
|
13
|
+
args: [--fix]
|
|
14
|
+
- id: ruff-format
|
|
15
|
+
- repo: https://github.com/compilerla/conventional-pre-commit
|
|
16
|
+
rev: v4.4.0
|
|
17
|
+
hooks:
|
|
18
|
+
- id: conventional-pre-commit
|
|
19
|
+
stages: [commit-msg]
|
|
20
|
+
args: []
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Required
|
|
2
|
+
version: 2
|
|
3
|
+
|
|
4
|
+
# Set the OS, Python version and other tools you might need
|
|
5
|
+
build:
|
|
6
|
+
os: ubuntu-22.04
|
|
7
|
+
tools:
|
|
8
|
+
python: "3.12"
|
|
9
|
+
|
|
10
|
+
python:
|
|
11
|
+
install:
|
|
12
|
+
- method: pip
|
|
13
|
+
path: .
|
|
14
|
+
extra_requirements:
|
|
15
|
+
# Install with the [docs] flag for sphinx docs specific extensions
|
|
16
|
+
- docs
|
|
17
|
+
|
|
18
|
+
# Build documentation in the "docs/" directory with Sphinx
|
|
19
|
+
sphinx:
|
|
20
|
+
configuration: docs/conf.py
|
skyscapes-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corey Spohn
|
|
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.
|
skyscapes-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skyscapes
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Astrophysical scene modeling for HWO direct imaging
|
|
5
|
+
Project-URL: Homepage, https://github.com/CoreySpohn/skyscapes
|
|
6
|
+
Project-URL: Issues, https://github.com/CoreySpohn/skyscapes/issues
|
|
7
|
+
Author-email: Corey Spohn <corey.a.spohn@nasa.gov>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Corey Spohn
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Intended Audience :: Science/Research
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
35
|
+
Requires-Python: >=3.11
|
|
36
|
+
Requires-Dist: hwoutils
|
|
37
|
+
Requires-Dist: jax>=0.8.1
|
|
38
|
+
Requires-Dist: jaxlib>=0.8.1
|
|
39
|
+
Requires-Dist: optixstuff
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: ipython; extra == 'docs'
|
|
44
|
+
Requires-Dist: matplotlib; extra == 'docs'
|
|
45
|
+
Requires-Dist: myst-nb; extra == 'docs'
|
|
46
|
+
Requires-Dist: sphinx; extra == 'docs'
|
|
47
|
+
Requires-Dist: sphinx-autoapi; extra == 'docs'
|
|
48
|
+
Requires-Dist: sphinx-autodoc-typehints; extra == 'docs'
|
|
49
|
+
Requires-Dist: sphinx-book-theme; extra == 'docs'
|
|
50
|
+
Provides-Extra: test
|
|
51
|
+
Requires-Dist: hypothesis; extra == 'test'
|
|
52
|
+
Requires-Dist: nox; extra == 'test'
|
|
53
|
+
Requires-Dist: orbix; extra == 'test'
|
|
54
|
+
Requires-Dist: pytest; extra == 'test'
|
|
55
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# skyscapes
|
|
59
|
+
Astrophysical scene modeling for HWO direct imaging. This library is set up to be a JAX based implementation of the [pyEDITH package](https://github.com/eleonoraalei/pyEDITH/).
|
|
60
|
+
|
|
61
|
+
## Overview
|
|
62
|
+
This package is currently in early development.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['hatchling', "hatch-fancy-pypi-readme", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "skyscapes"
|
|
7
|
+
description = "Astrophysical scene modeling for HWO direct imaging"
|
|
8
|
+
authors = [{ name = "Corey Spohn", email = "corey.a.spohn@nasa.gov" }]
|
|
9
|
+
dynamic = ['readme', 'version']
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["hwoutils", "jax>=0.8.1", "jaxlib>=0.8.1", "optixstuff"]
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pre-commit"]
|
|
22
|
+
docs = [
|
|
23
|
+
"sphinx",
|
|
24
|
+
"myst-nb",
|
|
25
|
+
"sphinx-book-theme",
|
|
26
|
+
"sphinx-autoapi",
|
|
27
|
+
"sphinx_autodoc_typehints",
|
|
28
|
+
"ipython",
|
|
29
|
+
"matplotlib",
|
|
30
|
+
]
|
|
31
|
+
test = ["nox", "pytest", "hypothesis", "pytest-cov", "orbix"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/CoreySpohn/skyscapes"
|
|
35
|
+
Issues = "https://github.com/CoreySpohn/skyscapes/issues"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
source = "vcs"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.hooks.vcs]
|
|
41
|
+
version-file = "src/skyscapes/_version.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
|
|
44
|
+
content-type = "text/markdown"
|
|
45
|
+
|
|
46
|
+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
|
|
47
|
+
path = "README.md"
|
|
48
|
+
|
|
49
|
+
[tool.ruff.lint]
|
|
50
|
+
select = ["B", "D", "E", "F", "I", "UP", "RUF"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint.pydocstyle]
|
|
53
|
+
convention = "google"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/skyscapes"]
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.sdist]
|
|
59
|
+
exclude = ["/scripts", "/docs", "/tests", "/.github"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Astrophysical scene modeling for HWO direct imaging.
|
|
2
|
+
|
|
3
|
+
>>> from skyscapes import System, Star, Planet, Disk, from_exovista
|
|
4
|
+
"""
|
|
5
|
+
try:
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
except ImportError:
|
|
8
|
+
__version__ = "unknown"
|
|
9
|
+
|
|
10
|
+
from .disk import Disk
|
|
11
|
+
from .loaders import from_exovista, get_earth_like_planet_indices
|
|
12
|
+
from .planet import Planet
|
|
13
|
+
from .star import Star
|
|
14
|
+
from .system import System
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Disk",
|
|
18
|
+
"Planet",
|
|
19
|
+
"Star",
|
|
20
|
+
"System",
|
|
21
|
+
"from_exovista",
|
|
22
|
+
"get_earth_like_planet_indices",
|
|
23
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""JAX-friendly debris-disk model for skyscapes.
|
|
2
|
+
|
|
3
|
+
Equinox module holding a wavelength-interpolated contrast cube.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import equinox as eqx
|
|
9
|
+
import interpax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
|
|
12
|
+
from .star import Star
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Disk(eqx.Module):
|
|
16
|
+
"""JAX-friendly debris disk (exozodiacal light).
|
|
17
|
+
|
|
18
|
+
Stores a wavelength-interpolated contrast cube relative to the host
|
|
19
|
+
star. Call :meth:`spec_flux_density` to get actual flux.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
star: Star
|
|
23
|
+
pixel_scale_arcsec: float
|
|
24
|
+
_wavelengths_nm: jnp.ndarray # (n_wl,)
|
|
25
|
+
_contrast_cube: jnp.ndarray # (n_wl, ny, nx)
|
|
26
|
+
_contrast_interp: interpax.CubicSpline
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
star: Star,
|
|
31
|
+
pixel_scale_arcsec: float,
|
|
32
|
+
wavelengths_nm: jnp.ndarray,
|
|
33
|
+
contrast_cube: jnp.ndarray,
|
|
34
|
+
):
|
|
35
|
+
"""
|
|
36
|
+
Args:
|
|
37
|
+
star: Host star.
|
|
38
|
+
pixel_scale_arcsec: Pixel scale of the contrast cube [arcsec/pixel].
|
|
39
|
+
wavelengths_nm: Wavelength grid [nm], shape ``(n_wl,)``.
|
|
40
|
+
contrast_cube: Disk-to-star contrast, shape ``(n_wl, ny, nx)``.
|
|
41
|
+
"""
|
|
42
|
+
self.star = star
|
|
43
|
+
self.pixel_scale_arcsec = pixel_scale_arcsec
|
|
44
|
+
self._wavelengths_nm = wavelengths_nm
|
|
45
|
+
self._contrast_cube = contrast_cube
|
|
46
|
+
self._contrast_interp = interpax.CubicSpline(
|
|
47
|
+
wavelengths_nm, contrast_cube, axis=0
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def spec_flux_density(self, wavelength_nm: float, time_jd: float) -> jnp.ndarray:
|
|
51
|
+
"""Disk flux density [ph/s/m²/nm], shape ``(ny, nx)``."""
|
|
52
|
+
contrast = self._contrast_interp(wavelength_nm)
|
|
53
|
+
star_flux = self.star.spec_flux_density(wavelength_nm, time_jd)
|
|
54
|
+
return contrast * star_flux
|
|
55
|
+
|
|
56
|
+
def spatial_extent(self) -> tuple[float, float]:
|
|
57
|
+
"""Spatial extent of the disk [arcsec]."""
|
|
58
|
+
ny, nx = self._contrast_cube.shape[-2:]
|
|
59
|
+
return (nx * self.pixel_scale_arcsec, ny * self.pixel_scale_arcsec)
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Load ExoVista FITS files into JAX-friendly exoverses objects.
|
|
2
|
+
|
|
3
|
+
This module migrates the core loading logic from
|
|
4
|
+
``coronagraphoto.loaders.exovista`` into exoverses, creating
|
|
5
|
+
:class:`~exoverses.jax.system.System` objects directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Optional, Sequence
|
|
12
|
+
|
|
13
|
+
import interpax
|
|
14
|
+
import jax.numpy as jnp
|
|
15
|
+
import numpy as np
|
|
16
|
+
from astropy.io.fits import getdata, getheader
|
|
17
|
+
from orbix.equations.orbit import mean_anomaly_tp
|
|
18
|
+
from orbix.system.planets import Planets as OrbixPlanets
|
|
19
|
+
|
|
20
|
+
from .disk import Disk
|
|
21
|
+
from .planet import Planet
|
|
22
|
+
from .star import Star, _Msun2kg, _decimal_year_to_jd, _mas2arcsec, _um2nm
|
|
23
|
+
from .system import System
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Orbital-element extraction ───────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
# Physical constants for state-vector → Keplerian conversion
|
|
29
|
+
_G = 6.67430e-11 # m³ kg⁻¹ s⁻²
|
|
30
|
+
_AU2m = 1.495978707e11
|
|
31
|
+
_Mearth2kg = 5.972167867791379e24
|
|
32
|
+
_au_per_yr_to_m_per_s = _AU2m / (365.25 * 86400.0)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _state_vector_to_keplerian(r, v, mu):
|
|
36
|
+
"""Convert (r, v) → (a, e, i, W, w, M) in SI, all angles in radians.
|
|
37
|
+
|
|
38
|
+
Ported from coronagraphoto.transforms.orbital_mechanics.
|
|
39
|
+
"""
|
|
40
|
+
r_mag = jnp.linalg.norm(r)
|
|
41
|
+
v_mag = jnp.linalg.norm(v)
|
|
42
|
+
|
|
43
|
+
h = jnp.cross(r, v)
|
|
44
|
+
h_mag = jnp.linalg.norm(h)
|
|
45
|
+
|
|
46
|
+
i = jnp.arccos(jnp.clip(h[2] / h_mag, -1.0, 1.0))
|
|
47
|
+
|
|
48
|
+
k = jnp.array([0.0, 0.0, 1.0])
|
|
49
|
+
n = jnp.cross(k, h)
|
|
50
|
+
n_mag = jnp.linalg.norm(n)
|
|
51
|
+
|
|
52
|
+
e_vec = (1 / mu) * ((v_mag**2 - mu / r_mag) * r - jnp.dot(r, v) * v)
|
|
53
|
+
e = jnp.linalg.norm(e_vec)
|
|
54
|
+
|
|
55
|
+
E_energy = 0.5 * v_mag**2 - mu / r_mag
|
|
56
|
+
a = jnp.where(jnp.abs(E_energy) > 1e-10, -mu / (2 * E_energy), jnp.inf)
|
|
57
|
+
|
|
58
|
+
TOL_E = 1e-9
|
|
59
|
+
TOL_I = 1e-9
|
|
60
|
+
is_circular = e < TOL_E
|
|
61
|
+
is_inclined = n_mag > TOL_I
|
|
62
|
+
|
|
63
|
+
W = jnp.where(is_inclined, jnp.arctan2(n[1], n[0]), 0.0)
|
|
64
|
+
|
|
65
|
+
cos_w = jnp.dot(n, e_vec) / (n_mag * e)
|
|
66
|
+
w_inclined = jnp.arccos(jnp.clip(cos_w, -1.0, 1.0))
|
|
67
|
+
w_inclined = jnp.where(e_vec[2] < 0, 2 * jnp.pi - w_inclined, w_inclined)
|
|
68
|
+
|
|
69
|
+
w_equatorial = jnp.arctan2(e_vec[1], e_vec[0])
|
|
70
|
+
w_equatorial = w_equatorial * jnp.sign(h[2])
|
|
71
|
+
|
|
72
|
+
w = jnp.where(is_circular, 0.0, jnp.where(is_inclined, w_inclined, w_equatorial))
|
|
73
|
+
|
|
74
|
+
cos_nu = jnp.dot(e_vec, r) / (e * r_mag)
|
|
75
|
+
nu_elliptical = jnp.arccos(jnp.clip(cos_nu, -1.0, 1.0))
|
|
76
|
+
nu_elliptical = jnp.where(
|
|
77
|
+
jnp.dot(r, v) < 0, 2 * jnp.pi - nu_elliptical, nu_elliptical
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
cos_u = jnp.dot(n, r) / (n_mag * r_mag)
|
|
81
|
+
u_inclined = jnp.arccos(jnp.clip(cos_u, -1.0, 1.0))
|
|
82
|
+
u_inclined = jnp.where(r[2] < 0, 2 * jnp.pi - u_inclined, u_inclined)
|
|
83
|
+
|
|
84
|
+
nu_equatorial = jnp.arctan2(r[1], r[0])
|
|
85
|
+
nu_equatorial = nu_equatorial * jnp.sign(h[2])
|
|
86
|
+
|
|
87
|
+
nu = jnp.where(
|
|
88
|
+
is_circular, jnp.where(is_inclined, u_inclined, nu_equatorial), nu_elliptical
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
W = W % (2 * jnp.pi)
|
|
92
|
+
w = w % (2 * jnp.pi)
|
|
93
|
+
nu = nu % (2 * jnp.pi)
|
|
94
|
+
|
|
95
|
+
E_angle = jnp.arctan2(jnp.sqrt(1 - e**2) * jnp.sin(nu), e + jnp.cos(nu))
|
|
96
|
+
M = E_angle - e * jnp.sin(E_angle)
|
|
97
|
+
M = M % (2 * jnp.pi)
|
|
98
|
+
M = jnp.where(e < 1.0, M, jnp.nan)
|
|
99
|
+
|
|
100
|
+
return a, e, i, W, w, M
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── Loaders ──────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _load_star(fits_file: str, fits_ext: int = 4) -> Star:
|
|
107
|
+
"""Load star from ExoVista FITS."""
|
|
108
|
+
with open(fits_file, "rb") as f:
|
|
109
|
+
obj_data, obj_header = getdata(f, ext=fits_ext, header=True, memmap=False)
|
|
110
|
+
wavelengths_um = getdata(f, ext=0, header=False, memmap=False)
|
|
111
|
+
|
|
112
|
+
wavelengths_nm = jnp.asarray(wavelengths_um * _um2nm)
|
|
113
|
+
times_year = jnp.asarray(2000.0 + obj_data[:, 0])
|
|
114
|
+
times_jd = _decimal_year_to_jd(times_year)
|
|
115
|
+
flux_density_jy = jnp.asarray(obj_data[:, 16:].T.astype(np.float32))
|
|
116
|
+
|
|
117
|
+
diameter_arcsec = obj_header["ANGDIAM"] * _mas2arcsec
|
|
118
|
+
mass_kg = obj_header.get("MASS") * _Msun2kg
|
|
119
|
+
dist_pc = obj_header.get("DIST")
|
|
120
|
+
midplane_pa = obj_header.get("PA", 0.0)
|
|
121
|
+
midplane_i = obj_header.get("I", 0.0)
|
|
122
|
+
ra_deg = obj_header.get("RA", 0.0)
|
|
123
|
+
dec_deg = obj_header.get("DEC", 0.0)
|
|
124
|
+
luminosity_lsun = obj_header.get("LSTAR", 1.0)
|
|
125
|
+
|
|
126
|
+
return Star(
|
|
127
|
+
dist_pc=dist_pc,
|
|
128
|
+
mass_kg=mass_kg,
|
|
129
|
+
ra_deg=ra_deg,
|
|
130
|
+
dec_deg=dec_deg,
|
|
131
|
+
midplane_pa_deg=midplane_pa,
|
|
132
|
+
midplane_i_deg=midplane_i,
|
|
133
|
+
diameter_arcsec=diameter_arcsec,
|
|
134
|
+
luminosity_lsun=luminosity_lsun,
|
|
135
|
+
wavelengths_nm=wavelengths_nm,
|
|
136
|
+
times_jd=times_jd,
|
|
137
|
+
flux_density_jy=flux_density_jy,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _load_planets(
|
|
142
|
+
fits_file: str,
|
|
143
|
+
star: Star,
|
|
144
|
+
planet_indices: Sequence[int],
|
|
145
|
+
required_planets: Optional[int] = None,
|
|
146
|
+
) -> Planet:
|
|
147
|
+
"""Load planets from ExoVista FITS."""
|
|
148
|
+
planet_ext_start = 5
|
|
149
|
+
oe_params: dict[str, list] = {
|
|
150
|
+
"a": [],
|
|
151
|
+
"e": [],
|
|
152
|
+
"i": [],
|
|
153
|
+
"W": [],
|
|
154
|
+
"w": [],
|
|
155
|
+
"M0": [],
|
|
156
|
+
"mass": [],
|
|
157
|
+
"radius": [],
|
|
158
|
+
"p": [],
|
|
159
|
+
}
|
|
160
|
+
contrast_grids: list[jnp.ndarray] = []
|
|
161
|
+
|
|
162
|
+
with open(fits_file, "rb") as f:
|
|
163
|
+
wavelengths_um = getdata(f, ext=0, header=False, memmap=False)
|
|
164
|
+
wavelengths_nm = jnp.asarray(wavelengths_um * _um2nm)
|
|
165
|
+
|
|
166
|
+
t0 = None
|
|
167
|
+
|
|
168
|
+
for idx in planet_indices:
|
|
169
|
+
with open(fits_file, "rb") as f:
|
|
170
|
+
obj_data, obj_header = getdata(
|
|
171
|
+
f, ext=planet_ext_start + idx, header=True, memmap=False
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
times_year = jnp.asarray(2000.0 + obj_data[:, 0])
|
|
175
|
+
times_jd = _decimal_year_to_jd(times_year)
|
|
176
|
+
if t0 is None:
|
|
177
|
+
t0 = times_jd[0]
|
|
178
|
+
|
|
179
|
+
contrast_data = jnp.asarray(obj_data[:, 16:].T.astype(np.float32))
|
|
180
|
+
|
|
181
|
+
# State vectors → orbital elements
|
|
182
|
+
r_sky_au = obj_data[0, 9:12]
|
|
183
|
+
v_sky_au_yr = obj_data[0, 12:15]
|
|
184
|
+
r_sky_m = jnp.array(r_sky_au * _AU2m)
|
|
185
|
+
v_sky_m_s = jnp.array(v_sky_au_yr * _au_per_yr_to_m_per_s)
|
|
186
|
+
mass_earth = obj_header.get("M")
|
|
187
|
+
planet_mass_kg = float(mass_earth) * _Mearth2kg
|
|
188
|
+
total_mass_kg = star.mass_kg + planet_mass_kg
|
|
189
|
+
mu = _G * total_mass_kg
|
|
190
|
+
_a, _e, i_rad, W_rad, w_rad, M_rad = _state_vector_to_keplerian(
|
|
191
|
+
r_sky_m, v_sky_m_s, mu
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
oe_params["a"].append(obj_header.get("A"))
|
|
195
|
+
oe_params["e"].append(obj_header.get("E"))
|
|
196
|
+
oe_params["i"].append(float(jnp.degrees(i_rad)))
|
|
197
|
+
oe_params["W"].append(float(jnp.degrees(W_rad)))
|
|
198
|
+
oe_params["w"].append(float(jnp.degrees(w_rad)))
|
|
199
|
+
oe_params["M0"].append(float(jnp.degrees(M_rad)))
|
|
200
|
+
oe_params["mass"].append(obj_header.get("M"))
|
|
201
|
+
oe_params["radius"].append(obj_header.get("R"))
|
|
202
|
+
oe_params["p"].append(obj_header.get("p", 0.2))
|
|
203
|
+
|
|
204
|
+
# Mean anomaly → regular grid for contrast interpolation
|
|
205
|
+
temp_planet = OrbixPlanets(
|
|
206
|
+
Ms=jnp.atleast_1d(star.mass_kg),
|
|
207
|
+
dist=jnp.atleast_1d(star.dist_pc),
|
|
208
|
+
a=jnp.atleast_1d(oe_params["a"][-1]),
|
|
209
|
+
e=jnp.atleast_1d(oe_params["e"][-1]),
|
|
210
|
+
W=jnp.atleast_1d(jnp.deg2rad(oe_params["W"][-1])),
|
|
211
|
+
i=jnp.atleast_1d(jnp.deg2rad(oe_params["i"][-1])),
|
|
212
|
+
w=jnp.atleast_1d(jnp.deg2rad(oe_params["w"][-1])),
|
|
213
|
+
M0=jnp.atleast_1d(jnp.deg2rad(oe_params["M0"][-1])),
|
|
214
|
+
t0=jnp.atleast_1d(t0),
|
|
215
|
+
Mp=jnp.atleast_1d(oe_params["mass"][-1]),
|
|
216
|
+
Rp=jnp.atleast_1d(oe_params["radius"][-1]),
|
|
217
|
+
p=jnp.atleast_1d(oe_params["p"][-1]),
|
|
218
|
+
)
|
|
219
|
+
mean_anom_coords = jnp.rad2deg(
|
|
220
|
+
mean_anomaly_tp(times_jd, temp_planet.n, temp_planet.tp) % (2 * jnp.pi)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Resample onto regular mean-anomaly grid
|
|
224
|
+
sort_idx = jnp.argsort(mean_anom_coords)
|
|
225
|
+
mean_anom_sorted = mean_anom_coords[sort_idx]
|
|
226
|
+
contrast_sorted = contrast_data[:, sort_idx]
|
|
227
|
+
|
|
228
|
+
mean_anomaly_grid = jnp.linspace(0, 360, 100)
|
|
229
|
+
xq, yq = jnp.meshgrid(wavelengths_nm, mean_anomaly_grid, indexing="ij")
|
|
230
|
+
contrast_grid = interpax.interp2d(
|
|
231
|
+
xq.flatten(),
|
|
232
|
+
yq.flatten(),
|
|
233
|
+
wavelengths_nm,
|
|
234
|
+
mean_anom_sorted,
|
|
235
|
+
contrast_sorted,
|
|
236
|
+
method="linear",
|
|
237
|
+
extrap=True,
|
|
238
|
+
).reshape(xq.shape)
|
|
239
|
+
contrast_grids.append(contrast_grid)
|
|
240
|
+
|
|
241
|
+
n_loaded = len(planet_indices)
|
|
242
|
+
|
|
243
|
+
# Ghost-planet padding for fixed array sizes
|
|
244
|
+
if required_planets is not None:
|
|
245
|
+
if n_loaded > required_planets:
|
|
246
|
+
warnings.warn(
|
|
247
|
+
f"Loaded {n_loaded} planets, but required_planets is {required_planets}. "
|
|
248
|
+
f"Truncating to first {required_planets} planets.",
|
|
249
|
+
UserWarning,
|
|
250
|
+
stacklevel=2,
|
|
251
|
+
)
|
|
252
|
+
for key in oe_params:
|
|
253
|
+
oe_params[key] = oe_params[key][:required_planets]
|
|
254
|
+
contrast_grids = contrast_grids[:required_planets]
|
|
255
|
+
n_loaded = required_planets
|
|
256
|
+
|
|
257
|
+
n_ghosts = required_planets - n_loaded
|
|
258
|
+
if n_ghosts > 0:
|
|
259
|
+
oe_params["a"].extend([1.0] * n_ghosts)
|
|
260
|
+
oe_params["e"].extend([0.0] * n_ghosts)
|
|
261
|
+
oe_params["i"].extend([0.0] * n_ghosts)
|
|
262
|
+
oe_params["W"].extend([0.0] * n_ghosts)
|
|
263
|
+
oe_params["w"].extend([0.0] * n_ghosts)
|
|
264
|
+
oe_params["M0"].extend([0.0] * n_ghosts)
|
|
265
|
+
oe_params["mass"].extend([0.0] * n_ghosts)
|
|
266
|
+
oe_params["radius"].extend([0.0] * n_ghosts)
|
|
267
|
+
oe_params["p"].extend([0.0] * n_ghosts)
|
|
268
|
+
|
|
269
|
+
base_shape = (
|
|
270
|
+
contrast_grids[0].shape if n_loaded > 0 else (len(wavelengths_nm), 100)
|
|
271
|
+
)
|
|
272
|
+
zero_grid = jnp.zeros(base_shape, dtype=jnp.float32)
|
|
273
|
+
contrast_grids.extend([zero_grid] * n_ghosts)
|
|
274
|
+
|
|
275
|
+
n_total = len(oe_params["a"])
|
|
276
|
+
|
|
277
|
+
# Build single OrbixPlanets object
|
|
278
|
+
orbix_planets = OrbixPlanets(
|
|
279
|
+
Ms=jnp.atleast_1d(star.mass_kg),
|
|
280
|
+
dist=jnp.atleast_1d(star.dist_pc),
|
|
281
|
+
a=jnp.array(oe_params["a"]),
|
|
282
|
+
e=jnp.array(oe_params["e"]),
|
|
283
|
+
W=jnp.deg2rad(jnp.array(oe_params["W"])),
|
|
284
|
+
i=jnp.deg2rad(jnp.array(oe_params["i"])),
|
|
285
|
+
w=jnp.deg2rad(jnp.array(oe_params["w"])),
|
|
286
|
+
M0=jnp.deg2rad(jnp.array(oe_params["M0"])),
|
|
287
|
+
t0=jnp.repeat(t0, n_total),
|
|
288
|
+
Mp=jnp.array(oe_params["mass"]),
|
|
289
|
+
Rp=jnp.array(oe_params["radius"]),
|
|
290
|
+
p=jnp.array(oe_params["p"]),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Stack contrast grids → 3D interpolator
|
|
294
|
+
if n_total == 1:
|
|
295
|
+
stacked = jnp.stack(contrast_grids * 2, axis=-1)
|
|
296
|
+
interp_indices = jnp.array([0, 1])
|
|
297
|
+
else:
|
|
298
|
+
stacked = jnp.stack(contrast_grids, axis=-1)
|
|
299
|
+
interp_indices = jnp.arange(n_total)
|
|
300
|
+
|
|
301
|
+
contrast_interp = interpax.Interpolator3D(
|
|
302
|
+
wavelengths_nm,
|
|
303
|
+
mean_anomaly_grid,
|
|
304
|
+
interp_indices,
|
|
305
|
+
stacked,
|
|
306
|
+
method="linear",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return Planet(
|
|
310
|
+
star=star,
|
|
311
|
+
orbix_planet=orbix_planets,
|
|
312
|
+
contrast_interp=contrast_interp,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _load_disk(fits_file: str, fits_ext: int, star: Star) -> Disk:
|
|
317
|
+
"""Load debris disk from ExoVista FITS."""
|
|
318
|
+
with open(fits_file, "rb") as f:
|
|
319
|
+
obj_data, header = getdata(f, ext=fits_ext, header=True, memmap=False)
|
|
320
|
+
wavelengths_um = getdata(f, ext=fits_ext - 1, header=False, memmap=False)
|
|
321
|
+
|
|
322
|
+
wavelengths_nm = jnp.asarray(wavelengths_um * _um2nm)
|
|
323
|
+
contrast_cube = jnp.asarray(obj_data[:-1].astype(np.float32))
|
|
324
|
+
pixel_scale_arcsec = header["PXSCLMAS"] * _mas2arcsec
|
|
325
|
+
|
|
326
|
+
return Disk(
|
|
327
|
+
star=star,
|
|
328
|
+
pixel_scale_arcsec=pixel_scale_arcsec,
|
|
329
|
+
wavelengths_nm=wavelengths_nm,
|
|
330
|
+
contrast_cube=contrast_cube,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_earth_like_planet_indices(fits_file: str) -> list[int]:
|
|
335
|
+
"""Identify Earth-like planets in an ExoVista FITS file.
|
|
336
|
+
|
|
337
|
+
Classification criteria (same as skyscapes):
|
|
338
|
+
• Scaled semi-major axis: 0.95 ≤ a / √L_star < 1.67 AU
|
|
339
|
+
• Planet radius: 0.8 / √a_scaled ≤ R < 1.4 R_earth
|
|
340
|
+
"""
|
|
341
|
+
with open(fits_file, "rb") as f:
|
|
342
|
+
h = getheader(f, ext=0, memmap=False)
|
|
343
|
+
_, star_header = getdata(f, ext=4, header=True, memmap=False)
|
|
344
|
+
|
|
345
|
+
n_ext = h["N_EXT"]
|
|
346
|
+
n_planets_total = n_ext - 4
|
|
347
|
+
star_luminosity_lsun = star_header.get("LSTAR", 1.0)
|
|
348
|
+
|
|
349
|
+
earth_indices: list[int] = []
|
|
350
|
+
for i in range(n_planets_total):
|
|
351
|
+
with open(fits_file, "rb") as f:
|
|
352
|
+
_, planet_header = getdata(f, ext=5 + i, header=True, memmap=False)
|
|
353
|
+
a_au = planet_header.get("A", 1.0)
|
|
354
|
+
radius_rearth = planet_header.get("R", 1.0)
|
|
355
|
+
a_scaled = a_au / np.sqrt(star_luminosity_lsun)
|
|
356
|
+
lower_r = 0.8 / np.sqrt(a_scaled)
|
|
357
|
+
is_earth = (0.95 <= a_scaled < 1.67) and (lower_r <= radius_rearth < 1.4)
|
|
358
|
+
if is_earth:
|
|
359
|
+
earth_indices.append(i)
|
|
360
|
+
|
|
361
|
+
return earth_indices
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def from_exovista(
|
|
365
|
+
fits_file: str,
|
|
366
|
+
planet_indices: Optional[Sequence[int]] = None,
|
|
367
|
+
required_planets: Optional[int] = None,
|
|
368
|
+
only_earths: bool = False,
|
|
369
|
+
) -> System:
|
|
370
|
+
"""Load an ExoVista FITS file into a JAX-friendly :class:`System`.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
fits_file: Path to ExoVista FITS file.
|
|
374
|
+
planet_indices: Planet indices to load (0-based). ``None`` = all.
|
|
375
|
+
required_planets: Pad/truncate to this many planets for fixed shapes.
|
|
376
|
+
only_earths: If True and *planet_indices* is None, auto-filter Earths.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
:class:`System` with star, planets, and disk.
|
|
380
|
+
"""
|
|
381
|
+
disk_ext = 2
|
|
382
|
+
|
|
383
|
+
with open(fits_file, "rb") as f:
|
|
384
|
+
h = getheader(f, ext=0, memmap=False)
|
|
385
|
+
n_ext = h["N_EXT"]
|
|
386
|
+
n_planets_total = n_ext - 4
|
|
387
|
+
|
|
388
|
+
if planet_indices is None:
|
|
389
|
+
if only_earths:
|
|
390
|
+
planet_indices = get_earth_like_planet_indices(fits_file)
|
|
391
|
+
else:
|
|
392
|
+
planet_indices = list(range(n_planets_total))
|
|
393
|
+
|
|
394
|
+
star = _load_star(fits_file, fits_ext=4)
|
|
395
|
+
planet = _load_planets(
|
|
396
|
+
fits_file, star, planet_indices, required_planets=required_planets
|
|
397
|
+
)
|
|
398
|
+
disk = _load_disk(fits_file, disk_ext, star)
|
|
399
|
+
|
|
400
|
+
return System(star=star, planet=planet, disk=disk)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""JAX-friendly planet model for skyscapes.
|
|
2
|
+
|
|
3
|
+
Equinox module wrapping ``orbix.Planets`` for orbital propagation and
|
|
4
|
+
an interpax 3-D interpolator for wavelength- and phase-dependent contrast.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import equinox as eqx
|
|
10
|
+
import interpax
|
|
11
|
+
import jax
|
|
12
|
+
import jax.numpy as jnp
|
|
13
|
+
from orbix.equations.orbit import mean_anomaly_tp
|
|
14
|
+
from orbix.kepler.shortcuts.grid import get_grid_solver
|
|
15
|
+
from orbix.system.planets import Planets as OrbixPlanets
|
|
16
|
+
|
|
17
|
+
from .star import Star
|
|
18
|
+
|
|
19
|
+
# Scalar trig solver for Kepler's equation
|
|
20
|
+
TRIG_SOLVER = get_grid_solver(level="scalar", E=False, trig=True, jit=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Planet(eqx.Module):
|
|
24
|
+
"""JAX-friendly collection of planets with contrast + orbital data.
|
|
25
|
+
|
|
26
|
+
Wraps ``orbix.Planets`` for Keplerian propagation and stores a 3-D
|
|
27
|
+
contrast interpolator (wavelength × mean anomaly × planet index).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
star: Star
|
|
31
|
+
orbix_planet: OrbixPlanets
|
|
32
|
+
contrast_interp: interpax.Interpolator3D
|
|
33
|
+
n_planets: int
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
star: Star,
|
|
38
|
+
orbix_planet: OrbixPlanets,
|
|
39
|
+
contrast_interp: interpax.Interpolator3D,
|
|
40
|
+
):
|
|
41
|
+
self.star = star
|
|
42
|
+
self.orbix_planet = orbix_planet
|
|
43
|
+
self.contrast_interp = contrast_interp
|
|
44
|
+
self.n_planets = orbix_planet.a.shape[0]
|
|
45
|
+
|
|
46
|
+
# ── Orbital propagation ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def mean_anomaly(self, time_jd: float) -> jnp.ndarray:
|
|
49
|
+
"""Mean anomalies at *time_jd* [deg], shape ``(n_planets,)``."""
|
|
50
|
+
return jnp.rad2deg(
|
|
51
|
+
mean_anomaly_tp(time_jd, self.orbix_planet.n, self.orbix_planet.tp)
|
|
52
|
+
% (2 * jnp.pi)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def position(self, time_jd: float) -> jnp.ndarray:
|
|
56
|
+
"""On-sky (dRA, dDec) in arcsec, shape ``(2, n_planets)``."""
|
|
57
|
+
ra, dec = self.orbix_planet.prop_ra_dec(TRIG_SOLVER, jnp.atleast_1d(time_jd))
|
|
58
|
+
return jnp.stack([ra[:, 0], dec[:, 0]])
|
|
59
|
+
|
|
60
|
+
def alpha_dMag(self, time_jd: float):
|
|
61
|
+
"""Angular separation [arcsec] and delta-mag, each ``(n_planets,)``."""
|
|
62
|
+
alpha, dMag = self.orbix_planet.alpha_dMag(TRIG_SOLVER, jnp.atleast_1d(time_jd))
|
|
63
|
+
return alpha[:, 0], dMag[:, 0]
|
|
64
|
+
|
|
65
|
+
# ── Spectral contrast ────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def contrast(self, wavelength_nm: float, time_jd: float) -> jnp.ndarray:
|
|
68
|
+
"""Planet-to-star contrast at given wavelength and time, ``(n_planets,)``."""
|
|
69
|
+
mean_anomalies_deg = self.mean_anomaly(time_jd)
|
|
70
|
+
planet_indices = jnp.arange(self.n_planets)
|
|
71
|
+
interp = jax.vmap(self.contrast_interp, in_axes=(None, 0, 0))
|
|
72
|
+
return interp(wavelength_nm, mean_anomalies_deg, planet_indices)
|
|
73
|
+
|
|
74
|
+
def spec_flux_density(self, wavelength_nm: float, time_jd: float) -> jnp.ndarray:
|
|
75
|
+
"""Planet flux density [ph/s/m²/nm], shape ``(n_planets,)``."""
|
|
76
|
+
c = self.contrast(wavelength_nm, time_jd)
|
|
77
|
+
star_flux = self.star.spec_flux_density(wavelength_nm, time_jd)
|
|
78
|
+
return c * star_flux
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""JAX-friendly star model for skyscapes.
|
|
2
|
+
|
|
3
|
+
Equinox module holding stellar spectral data with interpax interpolation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import equinox as eqx
|
|
9
|
+
import interpax
|
|
10
|
+
import jax
|
|
11
|
+
import jax.numpy as jnp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Physical constants (same values as coronagraphoto/orbix)
|
|
15
|
+
_Jy = 1e-26 # W m^-2 Hz^-1
|
|
16
|
+
_h = 6.62607015e-34 # J s
|
|
17
|
+
_Msun2kg = 1.988409870698051e30
|
|
18
|
+
_mas2arcsec = 1e-3
|
|
19
|
+
_um2nm = 1e3
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _jy_to_photons_per_nm_per_m2(flux_jy, wavelength_nm):
|
|
23
|
+
"""Convert Jy → ph/s/nm/m²."""
|
|
24
|
+
return flux_jy * _Jy / (wavelength_nm * _h)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _decimal_year_to_jd(decimal_year):
|
|
28
|
+
"""Convert decimal year → Julian Date (simplified J2000-based)."""
|
|
29
|
+
year = jnp.floor(decimal_year)
|
|
30
|
+
year_fraction = decimal_year - year
|
|
31
|
+
|
|
32
|
+
def _gregorian_to_jd(y, m, d):
|
|
33
|
+
a = jnp.floor((14 - m) / 12)
|
|
34
|
+
y2 = y + 4800 - a
|
|
35
|
+
m2 = m + 12 * a - 3
|
|
36
|
+
jdn = (
|
|
37
|
+
d
|
|
38
|
+
+ jnp.floor((153 * m2 + 2) / 5)
|
|
39
|
+
+ 365 * y2
|
|
40
|
+
+ jnp.floor(y2 / 4)
|
|
41
|
+
- jnp.floor(y2 / 100)
|
|
42
|
+
+ jnp.floor(y2 / 400)
|
|
43
|
+
- 32045
|
|
44
|
+
)
|
|
45
|
+
return jdn - 0.5
|
|
46
|
+
|
|
47
|
+
jd_start = _gregorian_to_jd(year, 1, 1)
|
|
48
|
+
jd_end = _gregorian_to_jd(year + 1, 1, 1)
|
|
49
|
+
return jd_start + year_fraction * (jd_end - jd_start)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Star(eqx.Module):
|
|
53
|
+
"""JAX-friendly stellar source.
|
|
54
|
+
|
|
55
|
+
Stores spectral flux density as ph/s/m²/nm via interpax interpolation
|
|
56
|
+
over wavelength (nm) and time (JD).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
dist_pc: float
|
|
60
|
+
mass_kg: float
|
|
61
|
+
ra_deg: float
|
|
62
|
+
dec_deg: float
|
|
63
|
+
midplane_pa_deg: float
|
|
64
|
+
midplane_i_deg: float
|
|
65
|
+
diameter_arcsec: float
|
|
66
|
+
luminosity_lsun: float
|
|
67
|
+
|
|
68
|
+
# Spectral data arrays
|
|
69
|
+
_wavelengths_nm: jnp.ndarray # (n_wl,)
|
|
70
|
+
_times_jd: jnp.ndarray # (n_t,)
|
|
71
|
+
_flux_density_phot: jnp.ndarray # (n_wl, n_t) ph/s/m²/nm
|
|
72
|
+
|
|
73
|
+
# Interpolator
|
|
74
|
+
_flux_interp: interpax.Interpolator2D
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
*,
|
|
79
|
+
dist_pc: float,
|
|
80
|
+
mass_kg: float,
|
|
81
|
+
ra_deg: float = 0.0,
|
|
82
|
+
dec_deg: float = 0.0,
|
|
83
|
+
midplane_pa_deg: float = 0.0,
|
|
84
|
+
midplane_i_deg: float = 0.0,
|
|
85
|
+
diameter_arcsec: float = 0.0,
|
|
86
|
+
luminosity_lsun: float = 1.0,
|
|
87
|
+
wavelengths_nm: jnp.ndarray,
|
|
88
|
+
times_jd: jnp.ndarray,
|
|
89
|
+
flux_density_jy: jnp.ndarray,
|
|
90
|
+
):
|
|
91
|
+
"""Initialize from Jansky flux.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
dist_pc: Distance to star [pc].
|
|
95
|
+
mass_kg: Stellar mass [kg].
|
|
96
|
+
ra_deg: Right ascension [deg].
|
|
97
|
+
dec_deg: Declination [deg].
|
|
98
|
+
midplane_pa_deg: System midplane position angle [deg].
|
|
99
|
+
midplane_i_deg: System midplane inclination [deg].
|
|
100
|
+
diameter_arcsec: Angular diameter [arcsec].
|
|
101
|
+
luminosity_lsun: Bolometric luminosity [L_sun].
|
|
102
|
+
wavelengths_nm: 1-D wavelength grid [nm].
|
|
103
|
+
times_jd: 1-D time grid [Julian days].
|
|
104
|
+
flux_density_jy: Flux density in Jy, shape (n_wl, n_t).
|
|
105
|
+
"""
|
|
106
|
+
self.dist_pc = dist_pc
|
|
107
|
+
self.mass_kg = mass_kg
|
|
108
|
+
self.ra_deg = ra_deg
|
|
109
|
+
self.dec_deg = dec_deg
|
|
110
|
+
self.midplane_pa_deg = midplane_pa_deg
|
|
111
|
+
self.midplane_i_deg = midplane_i_deg
|
|
112
|
+
self.diameter_arcsec = diameter_arcsec
|
|
113
|
+
self.luminosity_lsun = luminosity_lsun
|
|
114
|
+
self._wavelengths_nm = wavelengths_nm
|
|
115
|
+
self._times_jd = times_jd
|
|
116
|
+
|
|
117
|
+
# Jy → ph/s/nm/m² conversion (vectorized over time axis)
|
|
118
|
+
self._flux_density_phot = jax.vmap(
|
|
119
|
+
_jy_to_photons_per_nm_per_m2, in_axes=(1, None), out_axes=1
|
|
120
|
+
)(flux_density_jy, wavelengths_nm)
|
|
121
|
+
|
|
122
|
+
self._flux_interp = interpax.Interpolator2D(
|
|
123
|
+
wavelengths_nm, times_jd, self._flux_density_phot, method="cubic"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def spec_flux_density(self, wavelength_nm: float, time_jd: float) -> float:
|
|
127
|
+
"""Scalar spectral flux density [ph/s/m²/nm]."""
|
|
128
|
+
return self._flux_interp(wavelength_nm, time_jd)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""JAX-friendly planetary system model for skyscapes.
|
|
2
|
+
|
|
3
|
+
Simple container grouping a star, its planets, and an optional debris disk.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import equinox as eqx
|
|
11
|
+
|
|
12
|
+
from .disk import Disk
|
|
13
|
+
from .planet import Planet
|
|
14
|
+
from .star import Star
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class System(eqx.Module):
|
|
18
|
+
"""Complete planetary system: star + planets + disk.
|
|
19
|
+
|
|
20
|
+
This is the pure astrophysical system — no backgrounds, no observatory.
|
|
21
|
+
Background sources (zodiacal light, etc.) are handled by consumers
|
|
22
|
+
like coronagraphoto's ``SkyScene``.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
star: Star
|
|
26
|
+
planet: Planet
|
|
27
|
+
disk: Optional[Disk] = None
|