├── .devcontainer
└── devcontainer.json
├── .flake8
├── .github
└── workflows
│ ├── python-main-branch-package.yml
│ └── python-publish-pypi.yml
├── .gitignore
├── .isort.cfg
├── .pre-commit-config.yaml
├── .pydocstyle
├── .vscode
└── settings.json
├── CHANGELOG.rst
├── CONTRIBUTING.rst
├── Dockerfile_VSRemoteContainers
├── LICENSE
├── MANIFEST.in
├── README.md
├── SolarY
├── __init__.py
├── _config
│ ├── SPICE
│ │ ├── __init__.py
│ │ └── generic.ini
│ ├── __init__.py
│ ├── constants.ini
│ └── paths.ini
├── _version.py
├── asteroid
│ ├── __init__.py
│ └── physp.py
├── auxiliary
│ ├── __init__.py
│ ├── config.py
│ ├── download.py
│ ├── parse.py
│ └── reader.py
├── general
│ ├── __init__.py
│ ├── astrodyn.py
│ ├── geometry.py
│ ├── photometry.py
│ └── vec.py
├── instruments
│ ├── __init__.py
│ ├── camera.py
│ ├── optics.py
│ └── telescope.py
└── neo
│ ├── __init__.py
│ ├── astrodyn.py
│ └── data.py
├── docs
├── Makefile
├── README.rst
├── make.bat
├── requirements-docs.txt
└── source
│ ├── api.rst.old
│ ├── api
│ ├── asteroid
│ │ └── index.rst
│ ├── auxiliary
│ │ └── index.rst
│ ├── general
│ │ └── index.rst
│ ├── index.rst
│ ├── instruments
│ │ └── index.rst
│ └── neo
│ │ └── index.rst
│ ├── conf.py
│ └── index.rst
├── mypy.ini
├── pytest.ini
├── requirements.txt
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── _resources
│ ├── _config
│ │ └── test_paths.ini
│ ├── general
│ │ └── astrodyn_orbit_base_class.json
│ └── instruments
│ │ ├── camera_ccd.json
│ │ └── optics_reflector.json
├── test_asteroid
│ ├── __init__.py
│ └── test_physp.py
├── test_auxiliary
│ ├── __init__.py
│ ├── test_config.py
│ ├── test_download.py
│ ├── test_parse.py
│ └── test_reader.py
├── test_general
│ ├── __init__.py
│ ├── test_astrodyn.py
│ ├── test_geometry.py
│ ├── test_photometry.py
│ └── test_vec.py
├── test_instruments
│ ├── __init__.py
│ ├── test_camera.py
│ ├── test_optics.py
│ └── test_telescope.py
└── test_neo
│ ├── __init__.py
│ ├── test_astrodyn.py
│ └── test_data.py
├── tox.ini
└── versioneer.py
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.155.1/containers/docker-existing-dockerfile
3 | {
4 | "name": "Existing Dockerfile",
5 |
6 | // Sets the run context to one level up instead of the .devcontainer folder.
7 | "context": "..",
8 |
9 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
10 | "dockerFile": "../Dockerfile_VSRemoteContainers",
11 |
12 | // Set *default* container specific settings.json values on container create.
13 | "settings": {
14 | "terminal.integrated.shell.linux": null
15 | },
16 |
17 | // Add the IDs of extensions you want installed when the container is created.
18 | "extensions": [
19 | "ms-python.python",
20 | "hbenl.vscode-test-explorer",
21 | "littlefoxteam.vscode-python-test-adapter",
22 | "ms-python.vscode-pylance",
23 | "ms-toolsai.jupyter"
24 | ]
25 |
26 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
27 | // "forwardPorts": [],
28 |
29 | // Uncomment the next line to run commands after the container is created - for example installing curl.
30 | // "postCreateCommand": "apt-get update && apt-get install -y curl",
31 |
32 | // Uncomment when using a ptrace-based debugger like C++, Go, and Rust
33 | // "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
34 |
35 | // Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
36 | // "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
37 |
38 | // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
39 | // "remoteUser": "vscode"
40 | }
41 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length=120
3 | max-complexity=10
4 |
5 | # E203: Whitespace before ':'
6 | # E226 missing whitespace around arithmetic operator
7 | # W503 line break before binary operator
8 | # E123 closing bracket does not match indentation of opening bracket's line
9 | # ignore=E203,E226,W503,E123
10 |
--------------------------------------------------------------------------------
/.github/workflows/python-main-branch-package.yml:
--------------------------------------------------------------------------------
1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3 |
4 | name: Main Branch Package
5 |
6 | on:
7 | push:
8 | branches: [ main ]
9 | pull_request:
10 | branches: [ main ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: ['3.8']
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Set up Python ${{ matrix.python-version }}
23 | uses: actions/setup-python@v2
24 | with:
25 | python-version: ${{ matrix.python-version }}
26 | - name: Install Tox and any other packages
27 | run: pip install tox
28 | - name: Run Tox + PyTest
29 | # Run tox using the version of Python in `PATH`
30 | run: tox -e py
31 | - name: Run Tox + Flake8
32 | run: tox -e flake8
33 | - name: Run Tox + PyLint
34 | run: tox -e pylint
35 | - name: Run Tox + MyPy
36 | run: tox -e mypy
37 | - name: Run Tox + pydocstyle
38 | run: tox -e pydocstyle
39 | - name: Run Tox + bandit
40 | run: tox -e bandit
41 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish-pypi.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
3 |
4 | name: Upload Python Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | deploy:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Set up Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install setuptools wheel twine
25 | - name: Build and publish
26 | env:
27 | TWINE_USERNAME: __token__
28 | TWINE_PASSWORD: ${{ secrets.PYPI_APIKEY }}
29 | run: |
30 | python setup.py sdist bdist_wheel
31 | twine upload dist/*
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.DS_Store
2 | *.pyc
3 | build*
4 | dist*
5 | *.egg-info
6 | *.cat
7 | *.db
8 | *.dat
9 | .tox/.package.lock
10 | .coverage
11 | *htmlcov*
12 |
--------------------------------------------------------------------------------
/.isort.cfg:
--------------------------------------------------------------------------------
1 | [settings]
2 | default_section = THIRDPARTY
3 | known_first_party = mistral
4 | multi_line_output=3
5 | include_trailing_comma=True
6 | force_grid_wrap=0
7 | use_parentheses=True
8 | ensure_newline_before_comments = True
9 | line_length=88
10 | skip=__init__.py,versioneer.py,.ipynb_checkpoints,src,.tox,.eggs,.venv,build,dist
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | # Pre-commit (https://pre-commit.com)
2 | # Install:
3 | # pip install pre-commit
4 | # or
5 | # conda install pre-commit
6 | # Add a pre-commit configuration:
7 | # $ pre-commit install
8 | # (Optional) Run against all files
9 | # $ pre-commit run --all-files
10 |
11 | repos:
12 | # isort should run before black as black sometimes tweaks the isort output
13 | - repo: https://github.com/timothycrosley/isort
14 | rev: 5.7.0
15 | hooks:
16 | - id: isort
17 |
18 | # https://github.com/python/black#version-control-integration
19 | - repo: https://github.com/python/black
20 | rev: 20.8b1
21 | hooks:
22 | - id: black
23 |
24 | - repo: https://github.com/keewis/blackdoc
25 | rev: v0.3.2
26 | hooks:
27 | - id: blackdoc
--------------------------------------------------------------------------------
/.pydocstyle:
--------------------------------------------------------------------------------
1 | [pydocstyle]
2 | convention=numpy
3 | match=(?!_version).*.py
4 | add-ignore=D202,D105
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.testing.unittestEnabled": false,
3 | "python.testing.nosetestsEnabled": false,
4 | "python.testing.pytestEnabled": true
5 | }
--------------------------------------------------------------------------------
/CHANGELOG.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 0.6 / 2021-03-DD
5 | ----------------
6 |
7 | This is the first release with strict coding guidelines being enforced.
8 |
9 | New Features
10 | ------------
11 |
12 | * Added `tox.ini` to help automated CI.
13 |
14 | * Added automated coding guidelines enforcement using:
15 |
16 | - black
17 | - blackdoc
18 | - isort
19 |
20 | * Added tools to enforce strict coding standards:
21 |
22 | - flake8
23 | - mypy
24 | - pydocstyle
25 | - doc8
26 | - pylint
27 | - bandit
28 |
29 | Changes
30 | -------
31 |
32 | * Refactored `ReflectorCCD` to not use multiple inheritance but
33 | ``CCD`` and ``Reflector`` aggregation.
34 |
35 | * Restructured the directory structure to adhere to Python project
36 | standards.
37 |
38 | Fixes
39 | -----
40 |
41 | * none
42 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | ==================
2 | Contributing guide
3 | ==================
4 |
5 | TBW.
6 |
7 | Development Environment
8 | =======================
9 |
10 | TBW: constructing the development environment
11 |
12 | Running Example
13 | ---------------
14 |
15 | TBW: running the tests and CI tools.
16 |
17 |
18 | Contributing to Code
19 | ====================
20 |
21 | TBW.
22 |
23 | Git Workflow
24 | ------------
25 |
26 | Reference: https://gist.github.com/Chaser324/ce0505fbed06b947d962
27 |
28 | TBW: fork, branch, commit, pull request
29 |
30 |
31 | Contributing to Documentation
32 | =============================
33 |
34 | TBW.
35 |
--------------------------------------------------------------------------------
/Dockerfile_VSRemoteContainers:
--------------------------------------------------------------------------------
1 | # Get a Pyhton image
2 | FROM python:3.8-buster
3 |
4 | # Set some meta information
5 | LABEL description="SolarY - VS Code Remote-Containers developer environment"
6 | LABEL version="1.0"
7 |
8 | # Copy only the requirements.txt (used for pip install)
9 | COPY requirements.txt ./
10 |
11 | # Install all requirements
12 | RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
13 |
14 | # Set the Pythonpath and working directory
15 | ENV PYTHONPATH "/"
16 | WORKDIR .
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include SolarY/_config/constants.ini
2 | include SolarY/_config/paths.ini
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SolarY
2 | Welcome to SolarY, a small Open-Source Python library for asteroids, comets, meteoroids, meteors, cosmic dust and other minor bodies in the Solar System. This library is currently active and new modules are being developed and added frequently. Please check the library's main folder *solary/* for all available functions and sub-modules that are briefly described in the following.
3 |
4 | To install the package simply run:
5 |
6 | ```
7 | pip install solary
8 | ```
9 |
10 | ---
11 |
12 | ## asteroid
13 | This sub-module contains asteroid relevant information like:
14 | - Computation of the size of an asteroid depending on its albedo
15 |
16 | ## general
17 | General applicable functions and classes for:
18 | - Computation of the Tisserand Parameter of an object w.r.t. a larger object
19 | - Computation of an object's Sphere of Influence
20 | - Conversion of the apparent magnitude to irradiance
21 | - Computation of an object's apparent magnitude
22 | - Miscellaneous vector manipulation and computation functions
23 |
24 | ## instruments
25 | A module to compute e.g., telescope properties and their corresponding observational performance:
26 | - Camera
27 | - CCD
28 | - Telescope:
29 | - Reflector
30 |
31 | ## neo
32 | In this sub-module Near-Earth Object relevant functions can be found for e.g.:
33 | - Downloading recent NEO data and creating a local SQLite database
34 | - Downloading recent NEO simulation data and creating a local SQLite database
35 |
36 | ---
37 |
38 | ## Further contributions and the project's future
39 | The functionality of the library is currently rather basic and more will be added frequently. New sub-modules will also include functionalities for *comets*, *meteors*, as well as *cosmic dust*. Spacecraft mission data (e.g., from Cassini's Cosmic Dust Analyzer or the Rosetta mission) shall be included, too to grant an easy access for all passionate citizen scientists and others.
40 |
41 | ---
42 |
43 | ## Collaboration & Questions
44 | If you have any questions (e.g., how to use the package etc.) or if you would like to contribute something, feel free to contact me via [Twitter](https://twitter.com/MrAstroThomas) or [Reddit](https://www.reddit.com/user/MrAstroThomas).
45 |
46 | The Dockerfile Dockerfile_VSRemoteContainers is a developer environment / setup that can be used to develop on SolarY. The IDE VS Code has an extension called Remote-Containers that supports one to create reproducible and system-independent environments (based on the Container ecosystem). To work with this work install:
47 |
48 | - [Docker](https://www.docker.com/products/docker-desktop)
49 | - [VS Code](https://code.visualstudio.com/)
50 | - Extension: [Remote-Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
51 |
52 | The provided .devcontainer/devcontainer.json and .vscode/settings.json will then setup further extensions and settings within the container like e.g., the testing environment.
--------------------------------------------------------------------------------
/SolarY/__init__.py:
--------------------------------------------------------------------------------
1 | """SolarY."""
2 | # flake8: noqa
3 | from ._version import get_versions
4 |
5 | try:
6 | from . import asteroid, auxiliary, general, instruments, neo
7 | except ModuleNotFoundError as exc:
8 | # this occurs when during `tox -e build`, just ignore it.
9 | print("Import error in Solary.__init__. Exception:", exc)
10 |
11 | __version__ = get_versions()["version"] # type: ignore
12 | __date__ = get_versions()["date"] # type: ignore
13 | __project__ = "SolarY"
14 | __author__ = "Dr.-Ing. Thomas Albin"
15 | del get_versions
16 |
--------------------------------------------------------------------------------
/SolarY/_config/SPICE/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThomasAlbin/SolarY/779c0f0b0ca60222a688f4cf89cca7f9afb45781/SolarY/_config/SPICE/__init__.py
--------------------------------------------------------------------------------
/SolarY/_config/SPICE/generic.ini:
--------------------------------------------------------------------------------
1 | [leapseconds]
2 | url = https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls
3 | sha256 = 678e32bdb5a744117a467cd9601cd6b373f0e9bc9bbde1371d5eee39600a039b
4 | dir = solary_data/spice/generic_kernels/lsk
5 | file = naif0012.tls
6 |
7 | [planet_spk]
8 | url = https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/de432s.bsp
9 | sha256 = 363f32e14f5255359ac32c4d38080cf28ab55564a5e16696a75f63394b666e9b
10 | dir = solary_data/spice/generic_kernels/spk
11 | file = de432s.bsp
12 |
--------------------------------------------------------------------------------
/SolarY/_config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThomasAlbin/SolarY/779c0f0b0ca60222a688f4cf89cca7f9afb45781/SolarY/_config/__init__.py
--------------------------------------------------------------------------------
/SolarY/_config/constants.ini:
--------------------------------------------------------------------------------
1 | [constants]
2 | # The following constants have been obtained from:
3 | # The IAU 2009 system of astronomical constants: the report of the IAU working
4 | # group on numerical standards for Fundamental Astronomy; Brian Luzum,
5 | # Nicole Capitaine, A. Fienga, W. Folkner, T. Fukushima, J. Hilton,
6 | # C. Hohenkerk, G. Krasinsky, G. Petit, E. Pitjeva, M. Soffel, P. Wallace;
7 | # Celest Mech Dyn Astr (2011) 110:293–304, DOI 10.1007/s10569-011-9352-4
8 |
9 | # Heliocentric gravitational constant in km^3 * s^-2 (TDB compatible)
10 | gm_sun = 1.32712440041e+11
11 |
12 | # Heliocentric gravitational constant error in km^3 * s^-2 (TDB compatible)
13 | gm_sun_err = 1.0e+1
14 |
15 | # Geocentric gravitational constant in km^3 * s^-2 (TDB compatible)
16 | gm_earth = 3.986004356e5
17 |
18 | # Geocentric gravitational constant error in km^3 * s^-2 (TDB compatible)
19 | gm_earth_err = 8.0e-4
20 |
21 | # Gravitational constant in km^3 * kg^-1 * s^-2
22 | grav_const = 6.67428e-20
23 |
24 | # Gravitational constant error in km^3 * kg^-1 * s^-2
25 | grav_const_err = 6.7e-24
26 |
27 | # Astronomical Unit in km
28 | one_au = 1.49597870700e+8
29 |
30 |
31 | [photometry]
32 | # Zero point of the apparent bolometric magnitude given in W/m**2
33 | # https://www.iau.org/static/resolutions/IAU2015_English.pdf (page 2)
34 | appmag_irr_i0 = 2.518021002e-8
35 |
36 | # Number of photons per m**2 per second for different Johnson-Cousins passbands. From:
37 | # http://spiff.rit.edu/classes/phys440/lectures/filters/filters.html
38 | photon_flux_U = 5.5e9
39 | photon_flux_R = 1.17e10
40 | photon_flux_V = 8.66e9
41 | photon_flux_B = 1.1e10
42 | photon_flux_I = 6.75e9
43 |
44 | [planets]
45 | # Semi-major axis of Jupiter given in AU. Used e.g., for the Tisserand
46 | # parameter computations
47 | # https://nssdc.gsfc.nasa.gov/planetary/factsheet/jupiterfact.html
48 | sem_maj_axis_jup = 5.20336301
49 |
--------------------------------------------------------------------------------
/SolarY/_config/paths.ini:
--------------------------------------------------------------------------------
1 | [neo]
2 | neodys_raw_dir = solary_data/neo/data/
3 | neodys_raw_file = neodys.cat
4 |
5 | neodys_db_dir = solary_data/neo/databases
6 | neodys_db_file = neo_neodys.db
7 |
8 | granvik2018_raw_dir = solary_data/neo/data/
9 | granvik2018_raw_file = Granvik+_2018_Icarus.dat.gz
10 | granvik2018_unzip_file = Granvik+_2018_Icarus.dat
11 |
12 | granvik2018_db_dir = solary_data/neo/databases
13 | granvik2018_db_file = neo_granvik2018.db
14 |
--------------------------------------------------------------------------------
/SolarY/_version.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 |
3 | # This file helps to compute a version number in source trees obtained from
4 | # git-archive tarball (such as those provided by githubs download-from-tag
5 | # feature). Distribution tarballs (built by setup.py sdist) and build
6 | # directories (produced by setup.py build) will contain a much shorter file
7 | # that just contains the computed version number.
8 |
9 | # This file is released into the public domain. Generated by
10 | # versioneer-0.18 (https://github.com/warner/python-versioneer)
11 |
12 | """Git implementation of _version.py."""
13 |
14 | import errno
15 | import os
16 | import re
17 | import subprocess
18 | import sys
19 |
20 |
21 | def get_keywords():
22 | """Get the keywords needed to look up the version information."""
23 | # these strings will be replaced by git during git-archive.
24 | # setup.py/versioneer.py will grep for the variable names, so they must
25 | # each be defined on a line of their own. _version.py will just call
26 | # get_keywords().
27 | git_refnames = "$Format:%d$"
28 | git_full = "$Format:%H$"
29 | git_date = "$Format:%ci$"
30 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
31 | return keywords
32 |
33 |
34 | class VersioneerConfig:
35 | """Container for Versioneer configuration parameters."""
36 |
37 |
38 | def get_config():
39 | """Create, populate and return the VersioneerConfig() object."""
40 | # these strings are filled in when 'setup.py versioneer' creates
41 | # _version.py
42 | cfg = VersioneerConfig()
43 | cfg.VCS = "git"
44 | cfg.style = "pep440"
45 | cfg.tag_prefix = ""
46 | cfg.parentdir_prefix = "SolarY"
47 | cfg.versionfile_source = "SolarY/_version.py"
48 | cfg.verbose = False
49 | return cfg
50 |
51 |
52 | class NotThisMethod(Exception):
53 | """Exception raised if a method is not valid for the current scenario."""
54 |
55 |
56 | LONG_VERSION_PY = {} # type: dict
57 | HANDLERS = {} # type: dict
58 |
59 |
60 | def register_vcs_handler(vcs, method): # decorator
61 | """Decorator to mark a method as the handler for a particular VCS."""
62 |
63 | def decorate(f):
64 | """Store f in HANDLERS[vcs][method]."""
65 | if vcs not in HANDLERS:
66 | HANDLERS[vcs] = {}
67 | HANDLERS[vcs][method] = f
68 | return f
69 |
70 | return decorate
71 |
72 |
73 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None):
74 | """Call the given command(s)."""
75 | assert isinstance(commands, list)
76 | p = None
77 | for c in commands:
78 | try:
79 | dispcmd = str([c] + args)
80 | # remember shell=False, so use git.cmd on windows, not just git
81 | p = subprocess.Popen(
82 | [c] + args,
83 | cwd=cwd,
84 | env=env,
85 | stdout=subprocess.PIPE,
86 | stderr=(subprocess.PIPE if hide_stderr else None),
87 | )
88 | break
89 | except EnvironmentError:
90 | e = sys.exc_info()[1]
91 | if e.errno == errno.ENOENT:
92 | continue
93 | if verbose:
94 | print("unable to run %s" % dispcmd)
95 | print(e)
96 | return None, None
97 | else:
98 | if verbose:
99 | print("unable to find command, tried %s" % (commands,))
100 | return None, None
101 | stdout = p.communicate()[0].strip()
102 | if sys.version_info[0] >= 3:
103 | stdout = stdout.decode()
104 | if p.returncode != 0:
105 | if verbose:
106 | print("unable to run %s (error)" % dispcmd)
107 | print("stdout was %s" % stdout)
108 | return None, p.returncode
109 | return stdout, p.returncode
110 |
111 |
112 | def versions_from_parentdir(parentdir_prefix, root, verbose):
113 | """Try to determine the version from the parent directory name.
114 |
115 | Source tarballs conventionally unpack into a directory that includes both
116 | the project name and a version string. We will also support searching up
117 | two directory levels for an appropriately named parent directory
118 | """
119 | rootdirs = []
120 |
121 | for i in range(3):
122 | dirname = os.path.basename(root)
123 | if dirname.startswith(parentdir_prefix):
124 | return {
125 | "version": dirname[len(parentdir_prefix) :],
126 | "full-revisionid": None,
127 | "dirty": False,
128 | "error": None,
129 | "date": None,
130 | }
131 | else:
132 | rootdirs.append(root)
133 | root = os.path.dirname(root) # up a level
134 |
135 | if verbose:
136 | print(
137 | "Tried directories %s but none started with prefix %s"
138 | % (str(rootdirs), parentdir_prefix)
139 | )
140 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
141 |
142 |
143 | @register_vcs_handler("git", "get_keywords")
144 | def git_get_keywords(versionfile_abs):
145 | """Extract version information from the given file."""
146 | # the code embedded in _version.py can just fetch the value of these
147 | # keywords. When used from setup.py, we don't want to import _version.py,
148 | # so we do it with a regexp instead. This function is not used from
149 | # _version.py.
150 | keywords = {}
151 | try:
152 | f = open(versionfile_abs, "r")
153 | for line in f.readlines():
154 | if line.strip().startswith("git_refnames ="):
155 | mo = re.search(r'=\s*"(.*)"', line)
156 | if mo:
157 | keywords["refnames"] = mo.group(1)
158 | if line.strip().startswith("git_full ="):
159 | mo = re.search(r'=\s*"(.*)"', line)
160 | if mo:
161 | keywords["full"] = mo.group(1)
162 | if line.strip().startswith("git_date ="):
163 | mo = re.search(r'=\s*"(.*)"', line)
164 | if mo:
165 | keywords["date"] = mo.group(1)
166 | f.close()
167 | except EnvironmentError:
168 | pass
169 | return keywords
170 |
171 |
172 | @register_vcs_handler("git", "keywords")
173 | def git_versions_from_keywords(keywords, tag_prefix, verbose):
174 | """Get version information from git keywords."""
175 | if not keywords:
176 | raise NotThisMethod("no keywords at all, weird")
177 | date = keywords.get("date")
178 | if date is not None:
179 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
180 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601
181 | # -like" string, which we must then edit to make compliant), because
182 | # it's been around since git-1.5.3, and it's too difficult to
183 | # discover which version we're using, or to work around using an
184 | # older one.
185 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
186 | refnames = keywords["refnames"].strip()
187 | if refnames.startswith("$Format"):
188 | if verbose:
189 | print("keywords are unexpanded, not using")
190 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
191 | refs = set([r.strip() for r in refnames.strip("()").split(",")])
192 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
193 | # just "foo-1.0". If we see a "tag: " prefix, prefer those.
194 | TAG = "tag: "
195 | tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)])
196 | if not tags:
197 | # Either we're using git < 1.8.3, or there really are no tags. We use
198 | # a heuristic: assume all version tags have a digit. The old git %d
199 | # expansion behaves like git log --decorate=short and strips out the
200 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish
201 | # between branches and tags. By ignoring refnames without digits, we
202 | # filter out many common branch names like "release" and
203 | # "stabilization", as well as "HEAD" and "master".
204 | tags = set([r for r in refs if re.search(r"\d", r)])
205 | if verbose:
206 | print("discarding '%s', no digits" % ",".join(refs - tags))
207 | if verbose:
208 | print("likely tags: %s" % ",".join(sorted(tags)))
209 | for ref in sorted(tags):
210 | # sorting will prefer e.g. "2.0" over "2.0rc1"
211 | if ref.startswith(tag_prefix):
212 | r = ref[len(tag_prefix) :]
213 | if verbose:
214 | print("picking %s" % r)
215 | return {
216 | "version": r,
217 | "full-revisionid": keywords["full"].strip(),
218 | "dirty": False,
219 | "error": None,
220 | "date": date,
221 | }
222 | # no suitable tags, so version is "0+unknown", but full hex is still there
223 | if verbose:
224 | print("no suitable tags, using unknown + full revision id")
225 | return {
226 | "version": "0+unknown",
227 | "full-revisionid": keywords["full"].strip(),
228 | "dirty": False,
229 | "error": "no suitable tags",
230 | "date": None,
231 | }
232 |
233 |
234 | @register_vcs_handler("git", "pieces_from_vcs")
235 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
236 | """Get version from 'git describe' in the root of the source tree.
237 |
238 | This only gets called if the git-archive 'subst' keywords were *not*
239 | expanded, and _version.py hasn't already been rewritten with a short
240 | version string, meaning we're inside a checked out source tree.
241 | """
242 | GITS = ["git"]
243 | if sys.platform == "win32":
244 | GITS = ["git.cmd", "git.exe"]
245 |
246 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True)
247 | if rc != 0:
248 | if verbose:
249 | print("Directory %s not under git control" % root)
250 | raise NotThisMethod("'git rev-parse --git-dir' returned error")
251 |
252 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
253 | # if there isn't one, this yields HEX[-dirty] (no NUM)
254 | describe_out, rc = run_command(
255 | GITS,
256 | [
257 | "describe",
258 | "--tags",
259 | "--dirty",
260 | "--always",
261 | "--long",
262 | "--match",
263 | "%s*" % tag_prefix,
264 | ],
265 | cwd=root,
266 | )
267 | # --long was added in git-1.5.5
268 | if describe_out is None:
269 | raise NotThisMethod("'git describe' failed")
270 | describe_out = describe_out.strip()
271 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
272 | if full_out is None:
273 | raise NotThisMethod("'git rev-parse' failed")
274 | full_out = full_out.strip()
275 |
276 | pieces = {}
277 | pieces["long"] = full_out
278 | pieces["short"] = full_out[:7] # maybe improved later
279 | pieces["error"] = None
280 |
281 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
282 | # TAG might have hyphens.
283 | git_describe = describe_out
284 |
285 | # look for -dirty suffix
286 | dirty = git_describe.endswith("-dirty")
287 | pieces["dirty"] = dirty
288 | if dirty:
289 | git_describe = git_describe[: git_describe.rindex("-dirty")]
290 |
291 | # now we have TAG-NUM-gHEX or HEX
292 |
293 | if "-" in git_describe:
294 | # TAG-NUM-gHEX
295 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe)
296 | if not mo:
297 | # unparseable. Maybe git-describe is misbehaving?
298 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out
299 | return pieces
300 |
301 | # tag
302 | full_tag = mo.group(1)
303 | if not full_tag.startswith(tag_prefix):
304 | if verbose:
305 | fmt = "tag '%s' doesn't start with prefix '%s'"
306 | print(fmt % (full_tag, tag_prefix))
307 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % (
308 | full_tag,
309 | tag_prefix,
310 | )
311 | return pieces
312 | pieces["closest-tag"] = full_tag[len(tag_prefix) :]
313 |
314 | # distance: number of commits since tag
315 | pieces["distance"] = int(mo.group(2))
316 |
317 | # commit: short hex revision ID
318 | pieces["short"] = mo.group(3)
319 |
320 | else:
321 | # HEX: no tags
322 | pieces["closest-tag"] = None
323 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root)
324 | pieces["distance"] = int(count_out) # total number of commits
325 |
326 | # commit date: see ISO-8601 comment in git_versions_from_keywords()
327 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[
328 | 0
329 | ].strip()
330 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
331 |
332 | return pieces
333 |
334 |
335 | def plus_or_dot(pieces):
336 | """Return a + if we don't already have one, else return a ."""
337 | if "+" in pieces.get("closest-tag", ""):
338 | return "."
339 | return "+"
340 |
341 |
342 | def render_pep440(pieces):
343 | """Build up version string, with post-release "local version identifier".
344 |
345 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
346 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
347 |
348 | Exceptions:
349 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
350 | """
351 | if pieces["closest-tag"]:
352 | rendered = pieces["closest-tag"]
353 | if pieces["distance"] or pieces["dirty"]:
354 | rendered += plus_or_dot(pieces)
355 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
356 | if pieces["dirty"]:
357 | rendered += ".dirty"
358 | else:
359 | # exception #1
360 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"])
361 | if pieces["dirty"]:
362 | rendered += ".dirty"
363 | return rendered
364 |
365 |
366 | def render_pep440_pre(pieces):
367 | """TAG[.post.devDISTANCE] -- No -dirty.
368 |
369 | Exceptions:
370 | 1: no tags. 0.post.devDISTANCE
371 | """
372 | if pieces["closest-tag"]:
373 | rendered = pieces["closest-tag"]
374 | if pieces["distance"]:
375 | rendered += ".post.dev%d" % pieces["distance"]
376 | else:
377 | # exception #1
378 | rendered = "0.post.dev%d" % pieces["distance"]
379 | return rendered
380 |
381 |
382 | def render_pep440_post(pieces):
383 | """TAG[.postDISTANCE[.dev0]+gHEX] .
384 |
385 | The ".dev0" means dirty. Note that .dev0 sorts backwards
386 | (a dirty tree will appear "older" than the corresponding clean one),
387 | but you shouldn't be releasing software with -dirty anyways.
388 |
389 | Exceptions:
390 | 1: no tags. 0.postDISTANCE[.dev0]
391 | """
392 | if pieces["closest-tag"]:
393 | rendered = pieces["closest-tag"]
394 | if pieces["distance"] or pieces["dirty"]:
395 | rendered += ".post%d" % pieces["distance"]
396 | if pieces["dirty"]:
397 | rendered += ".dev0"
398 | rendered += plus_or_dot(pieces)
399 | rendered += "g%s" % pieces["short"]
400 | else:
401 | # exception #1
402 | rendered = "0.post%d" % pieces["distance"]
403 | if pieces["dirty"]:
404 | rendered += ".dev0"
405 | rendered += "+g%s" % pieces["short"]
406 | return rendered
407 |
408 |
409 | def render_pep440_old(pieces):
410 | """TAG[.postDISTANCE[.dev0]] .
411 |
412 | The ".dev0" means dirty.
413 |
414 | Eexceptions:
415 | 1: no tags. 0.postDISTANCE[.dev0]
416 | """
417 | if pieces["closest-tag"]:
418 | rendered = pieces["closest-tag"]
419 | if pieces["distance"] or pieces["dirty"]:
420 | rendered += ".post%d" % pieces["distance"]
421 | if pieces["dirty"]:
422 | rendered += ".dev0"
423 | else:
424 | # exception #1
425 | rendered = "0.post%d" % pieces["distance"]
426 | if pieces["dirty"]:
427 | rendered += ".dev0"
428 | return rendered
429 |
430 |
431 | def render_git_describe(pieces):
432 | """TAG[-DISTANCE-gHEX][-dirty].
433 |
434 | Like 'git describe --tags --dirty --always'.
435 |
436 | Exceptions:
437 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
438 | """
439 | if pieces["closest-tag"]:
440 | rendered = pieces["closest-tag"]
441 | if pieces["distance"]:
442 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
443 | else:
444 | # exception #1
445 | rendered = pieces["short"]
446 | if pieces["dirty"]:
447 | rendered += "-dirty"
448 | return rendered
449 |
450 |
451 | def render_git_describe_long(pieces):
452 | """TAG-DISTANCE-gHEX[-dirty].
453 |
454 | Like 'git describe --tags --dirty --always -long'.
455 | The distance/hash is unconditional.
456 |
457 | Exceptions:
458 | 1: no tags. HEX[-dirty] (note: no 'g' prefix)
459 | """
460 | if pieces["closest-tag"]:
461 | rendered = pieces["closest-tag"]
462 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
463 | else:
464 | # exception #1
465 | rendered = pieces["short"]
466 | if pieces["dirty"]:
467 | rendered += "-dirty"
468 | return rendered
469 |
470 |
471 | def render(pieces, style):
472 | """Render the given version pieces into the requested style."""
473 | if pieces["error"]:
474 | return {
475 | "version": "unknown",
476 | "full-revisionid": pieces.get("long"),
477 | "dirty": None,
478 | "error": pieces["error"],
479 | "date": None,
480 | }
481 |
482 | if not style or style == "default":
483 | style = "pep440" # the default
484 |
485 | if style == "pep440":
486 | rendered = render_pep440(pieces)
487 | elif style == "pep440-pre":
488 | rendered = render_pep440_pre(pieces)
489 | elif style == "pep440-post":
490 | rendered = render_pep440_post(pieces)
491 | elif style == "pep440-old":
492 | rendered = render_pep440_old(pieces)
493 | elif style == "git-describe":
494 | rendered = render_git_describe(pieces)
495 | elif style == "git-describe-long":
496 | rendered = render_git_describe_long(pieces)
497 | else:
498 | raise ValueError("unknown style '%s'" % style)
499 |
500 | return {
501 | "version": rendered,
502 | "full-revisionid": pieces["long"],
503 | "dirty": pieces["dirty"],
504 | "error": None,
505 | "date": pieces.get("date"),
506 | }
507 |
508 |
509 | def get_versions():
510 | """Get version information or return default if unable to do so."""
511 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
512 | # __file__, we can work backwards from there to the root. Some
513 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
514 | # case we can only use expanded keywords.
515 |
516 | cfg = get_config()
517 | verbose = cfg.verbose
518 |
519 | try:
520 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose)
521 | except NotThisMethod:
522 | pass
523 |
524 | try:
525 | root = os.path.realpath(__file__)
526 | # versionfile_source is the relative path from the top of the source
527 | # tree (where the .git directory might live) to this file. Invert
528 | # this to find the root from __file__.
529 | for i in cfg.versionfile_source.split("/"):
530 | root = os.path.dirname(root)
531 | except NameError:
532 | return {
533 | "version": "0+unknown",
534 | "full-revisionid": None,
535 | "dirty": None,
536 | "error": "unable to find root of source tree",
537 | "date": None,
538 | }
539 |
540 | try:
541 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
542 | return render(pieces, cfg.style)
543 | except NotThisMethod:
544 | pass
545 |
546 | try:
547 | if cfg.parentdir_prefix:
548 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
549 | except NotThisMethod:
550 | pass
551 |
552 | return {
553 | "version": "0+unknown",
554 | "full-revisionid": None,
555 | "dirty": None,
556 | "error": "unable to compute version",
557 | "date": None,
558 | }
559 |
--------------------------------------------------------------------------------
/SolarY/asteroid/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodule contains all asteroid related functions."""
2 | # flake8: noqa
3 | from . import physp
4 |
--------------------------------------------------------------------------------
/SolarY/asteroid/physp.py:
--------------------------------------------------------------------------------
1 | """Functions to describe and derive physical and instrinsic parameters of asteroids."""
2 | import math
3 |
4 |
5 | def ast_size(albedo: float, abs_mag: float) -> float:
6 | """
7 | Compute the radius of an asteroid by using the asteroid's albedo and absolute magnitude.
8 |
9 | Parameters
10 | ----------
11 | albedo : float
12 | Albedo of the object ranging within the intervall (0, 1].
13 | abs_mag : float
14 | Absolute magnitude of the object.
15 |
16 | Returns
17 | -------
18 | radius : float
19 | Radius of the object given in kilometer.
20 |
21 | References
22 | ----------
23 | [1] Chesley, Steven R.; Chodas, Paul W.; Milani, Andrea; Valsecchi, Giovanni B.; Yeomans,
24 | Donald K. (October 2002). Quantifying the Risk Posed by Potential Earth Impacts. Icarus.
25 | 159 (2): 425
26 |
27 | [2] https://cneos.jpl.nasa.gov/tools/ast_size_est.html
28 |
29 | [3] http://www.physics.sfasu.edu/astro/asteroids/sizemagnitude.html
30 |
31 | Examples
32 | --------
33 | >>> import SolarY
34 | >>> ast_radius = SolarY.asteroid.physp.ast_size(albedo=0.15, abs_mag=10)
35 | >>> ast_radius
36 | 17.157
37 | """
38 | # Compute the diameter in km
39 | diameter = (1329.0 / math.sqrt(albedo)) * 10.0 ** (-0.2 * abs_mag)
40 |
41 | # Convert the diameter to radius
42 | radius = diameter / 2.0
43 |
44 | return radius
45 |
--------------------------------------------------------------------------------
/SolarY/auxiliary/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodule contains auxiliary functionalities of SolarY."""
2 | # flake8: noqa
3 | from . import config, download, parse, reader
4 | from .config import root_dir
5 |
--------------------------------------------------------------------------------
/SolarY/auxiliary/config.py:
--------------------------------------------------------------------------------
1 | """Auxiliary functions for all library relevant configuration files."""
2 | import configparser
3 | import importlib
4 | import os
5 |
6 | root_dir = os.path.dirname(importlib.import_module("SolarY").__file__)
7 |
8 |
9 | def get_constants() -> configparser.ConfigParser:
10 | """
11 | Get the constants.ini file from the _config directory.
12 |
13 | Returns
14 | -------
15 | config : configparser.ConfigParser
16 | Configuration Parser that contains miscellaneous constants (like astrodynmical, time,
17 | etc.)
18 | """
19 | # Set config parser
20 | config = configparser.ConfigParser()
21 |
22 | # Get the constants ini file
23 | constants_ini_path = os.path.join(root_dir, "_config", "constants.ini")
24 |
25 | # Read and parse the config file
26 | config.read(constants_ini_path)
27 |
28 | return config
29 |
30 |
31 | def get_paths(test: bool = False) -> configparser.ConfigParser:
32 | """
33 | Get the ``paths.dir`` file from the _config directory.
34 |
35 | Parameters
36 | ----------
37 | test : bool
38 | Boolean value whether to use the default (prod.) or test configs. Default: False
39 |
40 | Returns
41 | -------
42 | config : configparser.ConfigParser
43 | Configuration Parser that contains miscellaneous paths to store / access / etc. downloaded
44 | files, created database etc.
45 | """
46 | # Set the config parser
47 | config = configparser.ConfigParser()
48 |
49 | # Get the paths ini file, differentiate between prod and test
50 | if test:
51 | paths_ini_path = os.path.join(
52 | root_dir, "../", "tests/_resources/_config", "test_paths.ini"
53 | )
54 | else:
55 | paths_ini_path = os.path.join(root_dir, "_config", "paths.ini")
56 |
57 | # Read and parse the config file
58 | config.read(paths_ini_path)
59 |
60 | return config
61 |
62 |
63 | def get_spice_kernels(ktype: str) -> configparser.ConfigParser:
64 | """
65 | Get the kernel information from the _config directory.
66 |
67 | Parameters
68 | ----------
69 | ktype : str
70 | SPICE Kernel type (e.g., "generic").
71 |
72 | Returns
73 | -------
74 | config : configparser.ConfigParser
75 | Configuration Parser that contains miscellaneous kernel information like the URL, type,
76 | directory and filename.
77 | """
78 | # Set the config parser
79 | config = configparser.ConfigParser()
80 |
81 | # Current kernel dictionary that encodes the present config files
82 | kernel_dict = {"generic": "generic.ini"}
83 |
84 | # Get the corresponding kernel config filepath
85 | ini_path = os.path.join(root_dir, "_config", "SPICE", kernel_dict.get(ktype, ""))
86 |
87 | # Read and parse the config file
88 | config.read(ini_path)
89 |
90 | return config
91 |
--------------------------------------------------------------------------------
/SolarY/auxiliary/download.py:
--------------------------------------------------------------------------------
1 | """Auxiliary functions to download miscellaneous datasets."""
2 | import typing as t
3 | import urllib.parse
4 | import urllib.request
5 |
6 | from . import config, parse
7 |
8 | # Get the file paths
9 | GENERIC_KERNEL_CONFIG = config.get_spice_kernels(ktype="generic")
10 |
11 |
12 | def spice_generic_kernels() -> t.Dict[str, str]:
13 | """
14 | Download the generic SPICE kernels into the solary data storage directory.
15 |
16 | The SPICE kernels will be saved to the following directory::
17 |
18 | ~HOME/solary_data/.
19 |
20 | Parameters
21 | ----------
22 | None.
23 |
24 | Returns
25 | -------
26 | Dict[str, str]
27 | The download file name with the associated MD5 hash.
28 | """
29 | # Set a placeholder list for the filepaths and corresponding / resulting MD5 values
30 | kernel_hashes = {}
31 |
32 | # Iterate through the SPICE config file. Each section corresponds to an individual SPICE kernel
33 | for kernel in GENERIC_KERNEL_CONFIG.sections():
34 |
35 | # Set the download filepath
36 | download_filename = parse.setnget_file_path(
37 | GENERIC_KERNEL_CONFIG[kernel]["dir"], GENERIC_KERNEL_CONFIG[kernel]["file"]
38 | )
39 |
40 | # Download the file and store it in the kernels directory
41 | downl_file_path, _ = urllib.request.urlretrieve( # nosec - no issue since static URL
42 | url=GENERIC_KERNEL_CONFIG[kernel]["url"], filename=download_filename
43 | )
44 |
45 | # Compute the MD5 hash value
46 | md5_hash = parse.comp_sha256(downl_file_path)
47 |
48 | # Append the filepath and MD5 in the dictionary
49 | kernel_hashes[download_filename] = md5_hash
50 |
51 | return kernel_hashes
52 |
--------------------------------------------------------------------------------
/SolarY/auxiliary/parse.py:
--------------------------------------------------------------------------------
1 | """Auxiliary functions for file and path parsing."""
2 | import hashlib
3 | import os
4 | import pathlib
5 | import typing as t
6 |
7 | from .config import root_dir
8 |
9 |
10 | def comp_sha256(file_name: t.Union[str, pathlib.Path]) -> str:
11 | """
12 | Compute the SHA256 hash of a file.
13 |
14 | Parameters
15 | ----------
16 | file_name : str
17 | Absolute or relative pathname of the file that shall be parsed.
18 |
19 | Returns
20 | -------
21 | sha256_res : str
22 | Resulting SHA256 hash.
23 | """
24 | # Set the MD5 hashing
25 | hash_sha256 = hashlib.sha256()
26 |
27 | # Open the file in binary mode (read-only) and parse it in 65,536 byte chunks (in case of
28 | # large files, the loading will not exceed the usable RAM)
29 | with pathlib.Path(file_name).open(mode="rb") as f_temp:
30 | for _seq in iter(lambda: f_temp.read(65536), b""):
31 | hash_sha256.update(_seq)
32 |
33 | # Digest the MD5 result
34 | sha256_res = hash_sha256.hexdigest()
35 |
36 | return sha256_res
37 |
38 |
39 | def setnget_file_path(dl_path: str, filename: str) -> str:
40 | """
41 | Compute the path of a file, depending on its download path.
42 |
43 | The standard download path is:
44 | ~$HOME/
45 |
46 | Parameters
47 | ----------
48 | dl_path : str
49 | Relative path of the directory where the file is stored (relative to ~$HOME/solary_data/).
50 | filename : str
51 | File name.
52 |
53 | Returns
54 | -------
55 | file_path : str
56 | Absolute file name path of the given file name and directory.
57 | """
58 | # Get the system's home directory
59 | home_dir = os.path.expanduser("~")
60 |
61 | # Join the home directory path with the download path
62 | compl_dl_path = os.path.join(home_dir, dl_path)
63 |
64 | # Create the download path, if it does not exists already (recursively)
65 | pathlib.Path(compl_dl_path).mkdir(parents=True, exist_ok=True)
66 |
67 | # Join the home dir. + download dir. with the filename
68 | file_path = os.path.join(compl_dl_path, filename)
69 |
70 | return file_path
71 |
72 |
73 | def get_test_file_path(file_path: str) -> str:
74 | """
75 | Compute the absolute path to a file within the testing suite.
76 |
77 | Parameters
78 | ----------
79 | file_path : str
80 | Relative filepath of the test file w.r.t. the root directory.
81 |
82 | Returns
83 | -------
84 | compl_test_file_path : str
85 | Absolute filepath to the testing file.
86 | """
87 | # Join the root directory of SolarY with the given filepath.
88 | # root_dir = os.path.dirname(importlib.import_module("SolarY").__file__)
89 | compl_test_file_path = os.path.join(root_dir, file_path)
90 |
91 | return compl_test_file_path
92 |
--------------------------------------------------------------------------------
/SolarY/auxiliary/reader.py:
--------------------------------------------------------------------------------
1 | """Auxiliary reader for miscellaneous files."""
2 | import json
3 | import typing as t
4 |
5 |
6 | def read_orbit(orbit_path: str) -> t.Tuple[t.Dict[str, float], t.Dict[str, float]]:
7 | """
8 | Read an orbit properties file.
9 |
10 | Parameters
11 | ----------
12 | orbit_path : str
13 | File path of the JSON file that contains the properties information.
14 |
15 | Returns
16 | -------
17 | orbit_values : dict
18 | Orbit values.
19 | orbit_units : dict
20 | Orbit units.
21 | """
22 | # Open the file path and load / read it as a JSON
23 | with open(orbit_path) as temp_obj:
24 | orbit_compl = json.load(temp_obj)
25 |
26 | # TODO: check if values and units are in json and then check if the keys are correct
27 | # Get the values and units dictionaries within the JSON file
28 | orbit_values = orbit_compl["values"]
29 | orbit_units = orbit_compl["units"]
30 |
31 | return orbit_values, orbit_units
32 |
--------------------------------------------------------------------------------
/SolarY/general/__init__.py:
--------------------------------------------------------------------------------
1 | """Generic functions are stored in this submodule like astrodynamics, geometry functions, etc."""
2 | # flake8: noqa
3 | from . import astrodyn, geometry, photometry, vec
4 |
--------------------------------------------------------------------------------
/SolarY/general/astrodyn.py:
--------------------------------------------------------------------------------
1 | """Miscellaneous functions regarding astro-dynamical topics can be found here."""
2 | import math
3 | import typing as t
4 |
5 | from .. import auxiliary as solary_auxiliary
6 |
7 |
8 | def tisserand(
9 | sem_maj_axis_obj: float,
10 | inc: float,
11 | ecc: float,
12 | sem_maj_axis_planet: t.Optional[float] = None,
13 | ) -> float:
14 | """
15 | Compute the Tisserand parameter of an object w.r.t. a larger object.
16 |
17 | If no semi-major axis of a larger object is given, the values for Jupiter are assumed.
18 |
19 | Parameters
20 | ----------
21 | sem_maj_axis_obj : float
22 | Semi-major axis of the minor object (whose Tisserand parameter shall be computed) given in
23 | AU.
24 | inc : float
25 | Inclination of the minor object given in radians.
26 | ecc : float
27 | Eccentricity of the minor object.
28 | sem_maj_axis_planet : float, optional
29 | Semi-major axis of the major object. If no value is given, the semi-major axis of Jupiter
30 | is taken. The default is None.
31 |
32 | Returns
33 | -------
34 | tisserand_parameter : float
35 | Tisserand parameter of the minor object w.r.t. the major object.
36 |
37 | Notes
38 | -----
39 | The Tisserand parameter provides a dimensionless value for the astro-dyamical relation between
40 | e.g., comets and Jupiter. Let's assume one wants to compute the Tisserand parameter of a comet
41 | with the largest gas giant. A Tisserand parameter between 2 and 3 (2 < Tisserand < 3) indicates
42 | a so called Jupiter-Family Comet (JFC).
43 |
44 | Examples
45 | --------
46 | An example to compute the Tisserand parameter of the comet 67P/Churyumov–Gerasimenko. The data
47 | have been obtained from https://ssd.jpl.nasa.gov/sbdb.cgi?sstr=67P
48 |
49 | >>> import math
50 | >>> import SolarY
51 | >>> tisserand_tsch_geras_67p = SolarY.general.astrodyn.tisserand(
52 | ... sem_maj_axis_obj=3.46, inc=math.radians(7.03), ecc=0.64
53 | ... )
54 | >>> tisserand_tsch_geras_67p
55 | 2.747580043374075
56 | """
57 | # If no semi-major axis of a larger object is given: Assume the planet Jupiter. Jupiter's
58 | # semi-major axis can be found in the config file
59 | if not sem_maj_axis_planet:
60 |
61 | # Get the constants config file
62 | config = solary_auxiliary.config.get_constants()
63 | sem_maj_axis_planet = float(config["planets"]["sem_maj_axis_jup"])
64 |
65 | # Compute the tisserand parameter
66 | tisserand_parameter = (sem_maj_axis_planet / sem_maj_axis_obj) + 2.0 * math.cos(
67 | inc
68 | ) * math.sqrt((sem_maj_axis_obj / sem_maj_axis_planet) * (1.0 - ecc ** 2.0))
69 |
70 | return tisserand_parameter
71 |
72 |
73 | def kep_apoapsis(sem_maj_axis: float, ecc: float) -> float:
74 | """
75 | Compute the apoapsis, depending on the semi-major axis and eccentricity.
76 |
77 | Parameters
78 | ----------
79 | sem_maj_axis : float
80 | Semi-major axis of the object in any unit.
81 | ecc : float
82 | Eccentricity of the object.
83 |
84 | Returns
85 | -------
86 | apoapsis : float
87 | Apoapsis of the object. Unit is identical to input unit of sem_maj_axis.
88 | """
89 | apoapsis = (1.0 + ecc) * sem_maj_axis
90 |
91 | return apoapsis
92 |
93 |
94 | def kep_periapsis(sem_maj_axis: float, ecc: float) -> float:
95 | """
96 | Compute the periapsis, depending on the semi-major axis and eccentricity.
97 |
98 | Parameters
99 | ----------
100 | sem_maj_axis : float
101 | Semi-major axis of the object in any unit.
102 | ecc : float
103 | Eccentricity of the object.
104 |
105 | Returns
106 | -------
107 | periapsis : float
108 | Periapsis of the object. Unit is identical to input unit of sem_maj_axis.
109 | """
110 | periapsis = (1.0 - ecc) * sem_maj_axis
111 |
112 | return periapsis
113 |
114 |
115 | def mjd2jd(m_juldate: float) -> float:
116 | """
117 | Convert the given Julian Date to the Modified Julian Date.
118 |
119 | Parameters
120 | ----------
121 | m_juldate : float
122 | Modified Julian Date.
123 |
124 | Returns
125 | -------
126 | juldate : float
127 | Julian Date.
128 | """
129 | juldate = m_juldate + 2400000.5
130 |
131 | return juldate
132 |
133 |
134 | def jd2mjd(juldate: float) -> float:
135 | """
136 | Convert the Modified Julian Date to the Julian Date.
137 |
138 | Parameters
139 | ----------
140 | juldate : float
141 | Julian Date.
142 |
143 | Returns
144 | -------
145 | m_juldate : float
146 | Modified Julian Date.
147 | """
148 | m_juldate = juldate - 2400000.5
149 |
150 | return m_juldate
151 |
152 |
153 | def sphere_of_influence(
154 | sem_maj_axis: float, minor_mass: float, major_mass: float
155 | ) -> float:
156 | """
157 | Compute the Sphere of Influence (SOI).
158 |
159 | Compute the Sphere of Influence (SOI) of a minor object w.r.t. a major object, assuming a
160 | spherical SOI
161 |
162 | Parameters
163 | ----------
164 | sem_maj_axis : float
165 | Semi-Major Axis given in any physical dimension.
166 | minor_mass : float
167 | Mass of the minor object given in any physical dimension.
168 | major_mass : float
169 | Mass of the major object given in the same physical dimension as minor_mass.
170 |
171 | Returns
172 | -------
173 | soi_radius : float
174 | SOI radius given in the same physical dimension as sem_maj_axis.
175 | """
176 | # Compute the Sphere of Influence (SOI)
177 | soi_radius = sem_maj_axis * ((minor_mass / major_mass) ** (2.0 / 5.0))
178 |
179 | return soi_radius
180 |
181 |
182 | class Orbit:
183 | """
184 | The Orbit class is a base class for further classes.
185 |
186 | Basic orbital computations are performed that are object type agnostic
187 | (like computating the apoapsis of an object). The base class requires
188 | the values of an orbit as well as the corresponding units for the spatial
189 | and angle information.
190 |
191 | Attributes
192 | ----------
193 | peri : float
194 | Periapsis. Given in any spatial dimension.
195 | ecc : float
196 | Eccentricity.
197 | incl : float
198 | Inclination. Given in degrees or radians.
199 | long_asc_node : float
200 | Longitude of ascending node. Given in the same units as incl.
201 | arg_peri : float
202 | Argument of periapsis. Given in the same uniuts as incl.
203 | units_dict : dict
204 | Dictionary that contains the keys "spatial" and "angle" that provide the units "km", "AU"
205 | and "rad" and "deg" respectively.
206 | """
207 |
208 | def __init__(
209 | self, orbit_values: t.Dict[str, float], orbit_units: t.Dict[str, float]
210 | ) -> None:
211 | """
212 | Init function.
213 |
214 | Parameters
215 | ----------
216 | orbit_values : dict
217 | Dictionary that contains the orbit's values. The spatial and angle unit are given by
218 | the orbit_units dictionary
219 | orbit_units : dict
220 | Dictionary that contains the orbit's values corresponding units (spatial and angle).
221 |
222 | Returns
223 | -------
224 | None.
225 | """
226 | # Setting attribute placeholders
227 | self.peri = orbit_values["peri"] # type: float
228 | self.ecc = orbit_values["ecc"] # type: float
229 | self.incl = orbit_values["incl"] # type: float
230 | self.long_asc_node = orbit_values["long_asc_node"] # type: float
231 | self.arg_peri = orbit_values["arg_peri"] # type: float
232 |
233 | # Set the units dictionary
234 | self.units_dict = orbit_units
235 |
236 | @property
237 | def semi_maj_axis(self) -> float:
238 | """
239 | Get the semi-major axis.
240 |
241 | Returns
242 | -------
243 | _semi_maj_axis : float
244 | Semi-major axis. Given in the same spatial dimension as the input parameters.
245 | """
246 | # Compute the semi-major axis
247 | _semi_maj_axis = self.peri / (1.0 - self.ecc)
248 |
249 | return _semi_maj_axis
250 |
251 | @property
252 | def apo(self) -> float:
253 | """
254 | Get the apoapsis.
255 |
256 | Returns
257 | -------
258 | _apo : float
259 | Apoapsis. Given in the the same spatial dimension as the input parameters.
260 | """
261 | # Comput the apoapsis
262 | _apo = kep_apoapsis(sem_maj_axis=self.semi_maj_axis, ecc=self.ecc)
263 |
264 | return _apo
265 |
--------------------------------------------------------------------------------
/SolarY/general/geometry.py:
--------------------------------------------------------------------------------
1 | """Auxiliary functions for geometric purposes."""
2 | import math
3 |
4 |
5 | def circle_area(radius: float) -> float:
6 | """
7 | Compute the area of a perfect with a given radius.
8 |
9 | Parameters
10 | ----------
11 | radius : float
12 | Radius of the circle given in any dimension.
13 |
14 | Returns
15 | -------
16 | area : float
17 | Area of the circle, given in the input dimension^2.
18 | """
19 | # Compute the area of a circle
20 | area = math.pi * (radius ** 2.0)
21 |
22 | return area
23 |
24 |
25 | def fwhm2std(fwhm: float) -> float:
26 | """
27 | Convert the Full Width at Half Maximum to the corresponding Gaussian standard deviation.
28 |
29 | Parameters
30 | ----------
31 | fwhm : float
32 | Full Width at Half Maximum.
33 |
34 | Returns
35 | -------
36 | gauss_sigma : float
37 | Standard deviation assuming a Gaussian distribution.
38 | """
39 | # Compute the standard deviation
40 | gauss_sigma = fwhm / (2.0 * math.sqrt(2.0 * math.log(2)))
41 |
42 | return gauss_sigma
43 |
--------------------------------------------------------------------------------
/SolarY/general/photometry.py:
--------------------------------------------------------------------------------
1 | """Functions for photometric purposes."""
2 | import math
3 | import typing as t
4 |
5 | from .. import auxiliary as solary_auxiliary
6 | from . import vec
7 |
8 |
9 | def appmag2irr(app_mag: t.Union[int, float]) -> float:
10 | """
11 | Convert the apparent magnitude to the corresponding irradiance.
12 |
13 | Convert the apparent magnitude to the corresponding irradiance given in
14 | W/m^2. The zero point magnitude is provided by the IAU in [1].
15 |
16 | Parameters
17 | ----------
18 | app_mag : int or float
19 | Apparent bolometric magnitude given in mag.
20 |
21 | Returns
22 | -------
23 | irradiance : float
24 | Irradiance given in W/m^2.
25 |
26 | References
27 | ----------
28 | [1] https://www.iau.org/static/resolutions/IAU2015_English.pdf
29 |
30 | Examples
31 | --------
32 | >>> import SolarY
33 | >>> irradiance = SolarY.general.photometry.appmag2irr(app_mag=8.0)
34 | >>> irradiance
35 | 1.5887638447672732e-11
36 | """
37 | # Load the configuration file that contains the zero point bolometric
38 | # irradiance
39 | config = solary_auxiliary.config.get_constants()
40 | appmag_irr_i0 = float(config["photometry"]["appmag_irr_i0"])
41 |
42 | # Convert apparent magnitude to irradiance
43 | irradiance = 10.0 ** (-0.4 * app_mag + math.log10(appmag_irr_i0))
44 |
45 | return irradiance
46 |
47 |
48 | def intmag2surmag(intmag: float, area: float) -> float:
49 | """
50 | Convert the integrated magnitude.
51 |
52 | Convert the integrated magnitude, given over a certain area in the sky to the corresponding
53 | surface brightness.
54 |
55 | Parameters
56 | ----------
57 | intmag : float
58 | Integrated magnitude given in mag.
59 | area : float
60 | Area of the object given in arcsec^2.
61 |
62 | Returns
63 | -------
64 | surface_mag : float
65 | Surface brightness of the object given in mag/arcsec^2.
66 | """
67 | # Compute the surface brightness
68 | surface_mag = intmag + 2.5 * math.log10(area)
69 |
70 | return surface_mag
71 |
72 |
73 | def surmag2intmag(surmag: float, area: float) -> float:
74 | """
75 | Convert the surface brightness and a sky area to an integrated magnitude.
76 |
77 | The integrated magnitude can later be used to e.g., convert the night
78 | sky background brightness to an irradiance.
79 |
80 | Parameters
81 | ----------
82 | surmag : float
83 | Surface brightness given in mag/arcsec^2.
84 | area : float
85 | Area given in arcsec^2.
86 |
87 | Returns
88 | -------
89 | intmag : float
90 | Integrated magnitude given in mag.
91 | """
92 | # Compute the integrated magnitude
93 | intmag = surmag - 2.5 * math.log10(area)
94 |
95 | return intmag
96 |
97 |
98 | def phase_func(index: int, phase_angle: float) -> float:
99 | """
100 | Phase function that is needed for the H-G visual / apparent magnitude function.
101 |
102 | The function has two versions, depending on the index ('1' or '2'). See [1].
103 |
104 | Parameters
105 | ----------
106 | index : str
107 | Phase function index / version. '1' or '2'.
108 | phase_angle : float
109 | Phase angle of the asteroid in radians (Angle as seen from the asteroid, pointing to
110 | a light source (Sun) and the observer (Earth)).
111 |
112 | Returns
113 | -------
114 | phi : float
115 | Phase function result.
116 |
117 | See Also
118 | --------
119 | hg_app_mag : Computing the visual / apparent magnitude of an object
120 |
121 | References
122 | ----------
123 | [1] https://www.britastro.org/asteroids/dymock4.pdf
124 |
125 | Examples
126 | --------
127 | >>> import math
128 | >>> import SolarY
129 | >>> phi1 = SolarY.general.photometry.phase_func(index=1, phase_angle=math.pi / 4.0)
130 | >>> phi1
131 | 0.14790968630394927
132 | >>> phi2 = SolarY.general.photometry.phase_func(index=2, phase_angle=math.pi / 4.0)
133 | >>> phi2
134 | 0.5283212147726485
135 | """
136 | # Dictionaries that contain the A and B constants, depending on the index version
137 | a_factor = {1: 3.33, 2: 1.87}
138 | b_factor = {1: 0.63, 2: 1.22}
139 |
140 | # Phase function
141 | phi = math.exp(
142 | -1.0 * a_factor[index] * ((math.tan(0.5 * phase_angle) ** b_factor[index]))
143 | )
144 |
145 | # Return the phase function result
146 | return phi
147 |
148 |
149 | def reduc_mag(abs_mag: float, phase_angle: float, slope_g: float = 0.15) -> float:
150 | """
151 | Compute the reduced magnitude of an object.
152 |
153 | This function is needed for the H-G visual / apparent magnitude function. See [1]
154 |
155 | Parameters
156 | ----------
157 | abs_mag : float
158 | Absolute magnitude of the object.
159 | phase_angle : float
160 | Phase angle of the object w.r.t. the illumination source and observer.
161 | slope_g : float, optional
162 | Slope parameter G for the reduced magnitude. The set default value can be applied for
163 | asteroids with unknown slope parameter and the interval is (0, 1). The default is 0.15.
164 |
165 | Returns
166 | -------
167 | reduced_magnitude : float
168 | Reduced magnitude of the object.
169 |
170 | See Also
171 | --------
172 | hg_app_mag : Computing the visual / apparent magnitude of an object
173 |
174 | References
175 | ----------
176 | [1] https://www.britastro.org/asteroids/dymock4.pdf
177 |
178 | Examples
179 | --------
180 | >>> import math
181 | >>> import SolarY
182 | >>> reduced_magnitude = SolarY.general.photometry.reduc_mag(
183 | ... abs_mag=10.0, phase_angle=math.pi / 4.0, slope_g=0.10
184 | ... )
185 | >>> reduced_magnitude
186 | 11.826504643588578
187 |
188 | Per default, the slope parameter G is set to 0.15 and fits well for most asteroids
189 |
190 | >>> reduced_magnitude = SolarY.general.photometry.reduc_mag(
191 | ... abs_mag=10.0, phase_angle=math.pi / 4.0
192 | ... )
193 | >>> reduced_magnitude
194 | 11.720766748872016
195 | """
196 | # Compute the reduced magnitude based on the equations given in the references [1]
197 | reduced_magnitude = abs_mag - 2.5 * math.log10(
198 | (1.0 - slope_g) * phase_func(index=1, phase_angle=phase_angle)
199 | + slope_g * phase_func(index=2, phase_angle=phase_angle)
200 | )
201 |
202 | return reduced_magnitude
203 |
204 |
205 | def hg_app_mag(
206 | abs_mag: float,
207 | vec_obj2obs: t.Union[t.List[float], t.Tuple[float, float, float]],
208 | vec_obj2ill: t.Union[t.List[float], t.Tuple[float, float, float]],
209 | slope_g: float = 0.15,
210 | ) -> float:
211 | """
212 | Compute the visual / apparent magnitude of an asteroid.
213 |
214 | This is based on the H-G system [1], where H represents the absolute magnitude
215 | and G represents the magnitude slope parameter.
216 |
217 | Parameters
218 | ----------
219 | abs_mag : float
220 | Absolute magnitude.
221 | vec_obj2obs : list
222 | 3 dimensional vector the contains the directional information (x, y, z) from the asteroid
223 | to the observer given in AU.
224 | vec_obj2ill : list
225 | 3 dimensional vector the contains the directional information (x, y, z) from the asteroid
226 | to the illumination source given in AU.
227 | slope_g : float, optional
228 | Slope parameter G for the reduced magnitude. The set default value can be applied for
229 | asteroids with unknown slope parameter and the interval is (0, 1). The default is 0.15.
230 |
231 | Returns
232 | -------
233 | app_mag : float
234 | Apparent / visual (bolometric) magnitude of the asteroid as seen from the observer.
235 |
236 | References
237 | ----------
238 | [1] https://www.britastro.org/asteroids/dymock4.pdf
239 |
240 | Examples
241 | --------
242 | >>> import SolarY
243 | >>> apparent_magnitude = SolarY.general.photometry.hg_app_mag(
244 | ... abs_mag=10.0,
245 | ... vec_obj2obs=[-1.0, 0.0, 0.0],
246 | ... vec_obj2ill=[-2.0, 0.0, 0.0],
247 | ... slope_g=0.10,
248 | ... )
249 | >>> apparent_magnitude
250 | 11.505149978319906
251 |
252 | """
253 | vec_obj2obs = list(vec_obj2obs)
254 | vec_obj2ill = list(vec_obj2ill)
255 |
256 | # Compute the length of the two input vectors
257 | vec_obj2obs_norm = vec.norm(vec_obj2obs)
258 | vec_obj2ill_norm = vec.norm(vec_obj2ill)
259 |
260 | # Compute the phase angle of the asteroid
261 | obj_phase_angle = vec.phase_angle(vec_obj2obs, vec_obj2ill)
262 |
263 | # Compute the reduced magnitude of the asteroid
264 | red_mag = reduc_mag(abs_mag, obj_phase_angle, slope_g)
265 |
266 | # Merge all information and compute the apparent magnitude of the asteroid as seen from the
267 | # observer
268 | app_mag = red_mag + 5.0 * math.log10(vec_obj2obs_norm * vec_obj2ill_norm)
269 |
270 | return app_mag
271 |
--------------------------------------------------------------------------------
/SolarY/general/vec.py:
--------------------------------------------------------------------------------
1 | """Auxiliary functions for vector computations."""
2 | import math
3 | import typing as t
4 |
5 |
6 | def norm(vector: t.List[float]) -> float:
7 | """
8 | Compute the norm of a given vector.
9 |
10 | The current version computes only the Euclidean Norm, respectivels the p2 norm.
11 |
12 | Parameters
13 | ----------
14 | vector : list
15 | Input vector of any dimensionality.
16 |
17 | Returns
18 | -------
19 | norm_res : float
20 | Norm of the input vector.
21 |
22 | Examples
23 | --------
24 | >>> import SolarY
25 | >>> vec_norm = SolarY.general.vec.norm(vector=[3.0, 5.0, -5.9])
26 | >>> vec_norm
27 | 8.295179322956196
28 | """
29 | # Compute the norm by summing all squared elements
30 | norm_res = math.sqrt(sum(abs(elem) ** 2.0 for elem in vector))
31 |
32 | return norm_res
33 |
34 |
35 | def unify(vector: t.List[float]) -> t.List[float]:
36 | """
37 | Normalise the input vector.
38 |
39 | The elements of the vector are divided by the norm of the vector.
40 | The result is a unit vector with the length 1.
41 |
42 | Parameters
43 | ----------
44 | vector : list
45 | Input vector of any dimensionality.
46 |
47 | Returns
48 | -------
49 | unit_vector : list
50 | Unified / Normalised vector.
51 |
52 | Examples
53 | --------
54 | >>> import SolarY
55 | >>> unit_vec = SolarY.general.vec.unify(vector=[1.0, 5.0, 10.0])
56 | >>> unit_vec
57 | [0.0890870806374748, 0.44543540318737396, 0.8908708063747479]
58 |
59 | Now check the norm of the resulting vector
60 |
61 | >>> vec_norm = SolarY.general.vec.norm(vector=unit_vec)
62 | >>> vec_norm
63 | 1.0
64 | """
65 | # Compute the norm of the input vector
66 | vector_norm = norm(vector)
67 |
68 | # Iterate through all input vector elements and normalise them
69 | unit_vector = [vector_elem / vector_norm for vector_elem in vector]
70 |
71 | return unit_vector
72 |
73 |
74 | def dot_prod(vector1: t.List[float], vector2: t.List[float]) -> float:
75 | """
76 | Compute the dot product between two given vectors.
77 |
78 | Parameters
79 | ----------
80 | vector1 : list
81 | Input vector #1 of any dimensionality.
82 | vector2 : list
83 | Input vector #2 with the same dimensionality as vector1.
84 |
85 | Returns
86 | -------
87 | dotp_res : float
88 | Dot product of both vectors.
89 |
90 | Examples
91 | --------
92 | >>> import SolarY
93 | >>> dot_product_res = SolarY.general.vec.dot_prod(
94 | ... vector1=[1.5, -4.0, 8.0], vector2=[-5.0, -4.20, 0.0]
95 | ... )
96 |
97 | >>> dot_product_res
98 | 9.3
99 | """
100 | # Compute dot product
101 | dotp_res = sum(v1_i * v2_i for v1_i, v2_i in zip(vector1, vector2))
102 |
103 | return dotp_res
104 |
105 |
106 | def phase_angle(vector1: t.List[float], vector2: t.List[float]) -> float:
107 | """
108 | Compute the phase angle between two vectors.
109 |
110 | The phase angle is the enclosed angle between the vectors at their
111 | corresponding point of origin.
112 |
113 | The output is given in radians and ranges from 0 to pi.
114 |
115 | Parameters
116 | ----------
117 | vector1 : list
118 | Input vector #1 of any dimensionality.
119 | vector2 : list
120 | Input vector #2 with the same dimensionality as vector1.
121 |
122 | Returns
123 | -------
124 | angle_rad : float
125 | Phase angle in radians between vector1 and vector2.
126 |
127 | Examples
128 | --------
129 | >>> import math
130 | >>> import SolarY
131 | >>> ph_angle_rad = SolarY.general.vec.phase_angle(
132 | ... vector1=[1.0, 0.0], vector2=[1.0, 1.0]
133 | ... )
134 | >>> ph_angle_deg = math.degrees(ph_angle_rad)
135 | >>> ph_angle_deg
136 | 45.0
137 | """
138 | # Compute the phase angle by considering the rearranged, known geometric definition of the dot
139 | # product
140 | angle_rad = math.acos(dot_prod(vector1, vector2) / (norm(vector1) * norm(vector2)))
141 |
142 | return angle_rad
143 |
144 |
145 | def substract(vector1: t.List[float], vector2: t.List[float]) -> t.List[float]:
146 | """
147 | Substracts the vector elements of one list with the elements of another list.
148 |
149 | Alternatively, one can use the Numpy library and Numpy arrays without using this function at
150 | all.
151 |
152 | Parameters
153 | ----------
154 | vector1 : list
155 | Input vector #1 of any dimensionality.
156 | vector2 : list
157 | Input vector #2 with the same dimensionality as vector1.
158 |
159 | Returns
160 | -------
161 | diff_vector : list
162 | Difference vector with the same dimensionality as vector1.
163 |
164 | Examples
165 | --------
166 | >>> import SolarY
167 | >>> vector_diff = SolarY.general.vec.substract(
168 | ... vector1=[1.0, 4.0, 2.0], vector2=[-8.0, 0.0, 1.0]
169 | ... )
170 | >>> vector_diff
171 | [9.0, 4.0, 1.0]
172 | """
173 | # Set an empty list for the vector difference / substraction
174 | diff_vector = []
175 |
176 | # Zip both input vector
177 | zipped_vector = zip(vector1, vector2)
178 |
179 | # Iterate through all elements and compute the substraction
180 | for vector1_i, vector2_i in zipped_vector:
181 | diff_vector.append(vector1_i - vector2_i)
182 |
183 | return diff_vector
184 |
185 |
186 | def inverse(vector: t.List[float]) -> t.List[float]:
187 | """
188 | Inverse the vector's elements. Alternatively, apply -1 on a Numpy array.
189 |
190 | Parameters
191 | ----------
192 | vector : list
193 | Input vector of any dimensionality.
194 |
195 | Returns
196 | -------
197 | inv_vector : TYPE
198 | Inverse output vector with the same dimensionality as vector.
199 |
200 | Examples
201 | --------
202 | >>> import SolarY
203 | >>> inverse_vector = SolarY.general.vec.inverse(vector=[1.0, 2.0, -3.0])
204 | >>> inverse_vector
205 | [-1.0, -2.0, 3.0]
206 | """
207 | # Inverse the vector element entries by multiplying -1.0 to each element
208 | inv_vector = [-1.0 * vector_elem for vector_elem in vector]
209 |
210 | return inv_vector
211 |
--------------------------------------------------------------------------------
/SolarY/instruments/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodule contains instrument related functionalities, like telescopes or in-situ instr."""
2 | # flake8: noqa
3 | from . import camera, optics, telescope
4 |
--------------------------------------------------------------------------------
/SolarY/instruments/camera.py:
--------------------------------------------------------------------------------
1 | """Miscellaneous functions and classes for / of camera system (e.g., CCDs).
2 |
3 | .. |dot| unicode:: U+2219 .. BULLET OPERATOR
4 | """
5 | import json
6 | import typing as t
7 | from pathlib import Path
8 |
9 |
10 | class CCD:
11 | """Class that defines ande describes a CCD camera.
12 |
13 | Properties are derived from the user's input like e.g., the chip size.
14 | """
15 |
16 | def __init__(
17 | self,
18 | pixels: t.Tuple[int, int],
19 | pixel_size: float,
20 | dark_noise: float,
21 | readout_noise: float,
22 | full_well: t.Union[int, float],
23 | quantum_eff: float,
24 | ) -> None:
25 | r"""CCD camera initializer.
26 |
27 | Parameters
28 | ----------
29 | pixels: (int, int)
30 | List with the number of pixels per dimension.
31 | pixel_size: float
32 | Size of a single pixel, assuming a square shaped pixel. Given in micro meter.
33 | dark_noise: float
34 | Dark noise / current of a single pixel.
35 | Given in: `electrons`\ :sup:`-1` |dot| `s`\ :sup:`-1` |dot| `pixel`\ :sup:`-1`
36 | readout_noise: float
37 | Readout noise of a single pixel. Given in electrons^-1 * pixel^-1.
38 | full_well: t.Union[int, float]
39 | Number of max. electrons per pixel.
40 | quantum_eff: float
41 | Quantum efficiency of the sensor. Allowed values: 0.0 qe < 1.0
42 | """
43 | self._pixels = pixels
44 | self._pixel_size = pixel_size
45 | self._dark_noise = dark_noise
46 | self._readout_noise = readout_noise
47 | self._full_well = full_well
48 | self._quantum_eff = quantum_eff
49 |
50 | @classmethod
51 | def load_from_json_file(cls, config_path: t.Union[Path, str]) -> t.Any:
52 | """Construct a CCD object from a JSON file."""
53 | with Path(config_path).open(mode="r") as temp_obj:
54 | ccd_config = json.load(temp_obj)
55 | return cls(**ccd_config)
56 |
57 | @property
58 | def pixels(self) -> t.Tuple[int, int]:
59 | """List with the number of pixels per dimension (w x h)."""
60 | return self._pixels
61 |
62 | @property
63 | def pixel_size(self) -> float:
64 | """Size of a single pixel, assuming a square shaped pixel in um."""
65 | return self._pixel_size
66 |
67 | @property
68 | def dark_noise(self) -> float:
69 | """Dark noise / current of a single pixel."""
70 | return self._dark_noise
71 |
72 | @property
73 | def readout_noise(self) -> float:
74 | """Readout noise of a single pixel in electrons^-1 * pixel^-1."""
75 | return self._readout_noise
76 |
77 | @property
78 | def full_well(self) -> float:
79 | """Return the maximum number of electrons per pixel."""
80 | return self._full_well
81 |
82 | @property
83 | def quantum_eff(self) -> float:
84 | """Quantum efficiency of the sensor 0 < QE < 1.0."""
85 | return self._quantum_eff
86 |
87 | @property
88 | def chip_size(self) -> t.Tuple[float, float]:
89 | """[float, float]: Get the chip size (x and y dimension). Given in mm."""
90 | # Placeholder list for the results
91 | chip_size = []
92 |
93 | # Convert the pixel size, given in micro meter, to mm.
94 | pixel_size_mm = self.pixel_size / 1000.0
95 |
96 | # Compute the chip size in each dimension by multiplying the number of
97 | # pixels with the pixel size, given in mm.
98 | for pixel_dim in self.pixels:
99 | chip_size.append(pixel_dim * pixel_size_mm)
100 |
101 | return chip_size[0], chip_size[1]
102 |
103 | @property
104 | def pixel_size_sq_m(self) -> float:
105 | """float: Get the size of a single pixel in m^2."""
106 | # Conversion between micro meter^2 to meter^2 -> 10^-12
107 | return (self.pixel_size ** 2.0) * (10.0 ** (-12))
108 |
--------------------------------------------------------------------------------
/SolarY/instruments/optics.py:
--------------------------------------------------------------------------------
1 | """Implements classes and functions that are needed for optical systems."""
2 | # pylint: disable=no-member
3 | import json
4 | import typing as t
5 | from pathlib import Path
6 |
7 | from ..general.geometry import circle_area
8 |
9 |
10 | class Reflector:
11 | """
12 | Class that defines the optical system of a reflector telescope.
13 |
14 | The class is a base class for a high level telescope class and dictionary
15 | configurations to set attributes. Properties are derived from the user's
16 | input like e.g. the collector area of a telescope.
17 | """
18 |
19 | def __init__(
20 | self,
21 | main_mirror_dia: float,
22 | sec_mirror_dia: float,
23 | optical_throughput: float,
24 | focal_length: float,
25 | ) -> None:
26 | """Init function.
27 |
28 | Parameters
29 | ----------
30 | main_mirror_dia : float
31 | Diameter of the main mirror. Given in m.
32 | sec_mirror_dia : float
33 | Diameter of the secondary mirror. Given in m.
34 | optical_throughput: float
35 | Throughput of the telescope. The Throughput is a combination of
36 | the mirror reflectivity, filter transmissivity etc. It describes
37 | only the optical system and does no include e.g., the quantum
38 | efficiency of a camera system.
39 | Dimensionless and defined between 0 and 1.
40 | focal_length : float
41 | Focal length of the system. Given in m.
42 | """
43 | self._main_mirror_dia = main_mirror_dia
44 | self._sec_mirror_dia = sec_mirror_dia
45 | self._optical_throughput = optical_throughput
46 | self._focal_length = focal_length
47 |
48 | @classmethod
49 | def load_from_json_file(cls, config_path: t.Union[Path, str]) -> t.Any:
50 | """Construct a Reflector object from a JSON file.
51 |
52 | Parameters
53 | ----------
54 | config_path: t.Union[Path, str]
55 | The JSON configuration file location.
56 |
57 | Returns
58 | -------
59 | Reflector
60 | A Reflector instance.
61 | """
62 | with Path(config_path).open(mode="r") as temp_obj:
63 | config = json.load(temp_obj)
64 | return cls(**config)
65 |
66 | @property
67 | def main_mirror_dia(self) -> float:
68 | """Diameter of the main mirror in meters."""
69 | return self._main_mirror_dia
70 |
71 | @property
72 | def sec_mirror_dia(self) -> float:
73 | """Diameter of the secondary mirror in meters."""
74 | return self._sec_mirror_dia
75 |
76 | @property
77 | def optical_throughput(self) -> float:
78 | """Throughput of the telescope.
79 |
80 | The Throughput is a combination of the mirror reflectivity,
81 | filter transmissivity etc. It describes only the optical system and does no include e.g.,
82 | the quantum efficiency of a camera system. Dimensionless and defined between 0 and 1.
83 | """
84 | return self._optical_throughput
85 |
86 | @property
87 | def focal_length(self) -> float:
88 | """Focal length of the system in meters."""
89 | return self._focal_length
90 |
91 | @property
92 | def main_mirror_area(self) -> float:
93 | """Get the main mirror area, assuming a circular shaped mirror.
94 |
95 | Returns
96 | -------
97 | float
98 | Main mirror area. Given in m^2.
99 | """
100 | # Call a sub-module that requires the radius as an input
101 | return circle_area(self.main_mirror_dia / 2.0)
102 |
103 | @property
104 | def sec_mirror_area(self) -> float:
105 | """Get the secondary mirror area in m^2, assuming a circular shaped mirror."""
106 | # Call a sub-module that requires the radius as an input
107 | return circle_area(self.sec_mirror_dia / 2.0)
108 |
109 | @property
110 | def collect_area(self) -> float:
111 | """Get the photon collection area in m^2.
112 |
113 | This property simply substracts the secondary mirror area from the main one.
114 | """
115 | return self.main_mirror_area - self.sec_mirror_area
116 |
--------------------------------------------------------------------------------
/SolarY/instruments/telescope.py:
--------------------------------------------------------------------------------
1 | """Implements telescope classes that mostly consists of base classes.
2 |
3 | Currently implemented sub-systems,
4 |
5 | * optical systems
6 | * cameras
7 | """
8 | import math
9 | import typing as t
10 | from pathlib import Path
11 |
12 | from .. import auxiliary as solary_auxiliary
13 | from .. import general as solary_general
14 | from .camera import CCD
15 | from .optics import Reflector
16 |
17 |
18 | def comp_fov(sensor_dim: float, focal_length: float) -> float:
19 | """Compute the Field-Of-View (FOV).
20 |
21 | This is dependent on the camera's chip size and telescope's focal length.
22 | See [1].
23 |
24 | Parameters
25 | ----------
26 | sensor_dim : float
27 | Sensor size (e.g., x or y dimension). Given in mm.
28 | focal_length : float
29 | Focal length of the telescope. Given in mm.
30 |
31 | Returns
32 | -------
33 | fov_arcsec : float
34 | Resulting FOV. Given in arcsec.
35 |
36 | References
37 | ----------
38 | [1] https://www.celestron.com/blogs/knowledgebase/
39 | how-do-i-determine-the-field-of-view-for-my-ccd-chip-and-telescope; 04.Jan.2021
40 | """
41 | # Compute the FOV. The equation, provided by [1], has been converted from imperial to SI.
42 | fov_arcsec = (3436.62 * sensor_dim / focal_length) * 60.0
43 |
44 | return fov_arcsec
45 |
46 |
47 | class ReflectorCCD:
48 | """Reflector telescope with camera system.
49 |
50 | Class of a reflector telescope with a CCD camera system. This class loads config files and sets
51 | attributes of a telescope system. Further, one can set observationsl settings like e.g., the
52 | exposure time. Functions allow one to compute the Signal-To-Noise ratio of an object.
53 |
54 | Since this class is build on the Reflector and CCD class, please check "See Also" to see the
55 | class references. These base classes contain more attributes, properties, etc. and can be read
56 | in the corresponding docstring.
57 |
58 | Attributes
59 | ----------
60 | _photo_flux_v : float
61 | Photon flux of a 0 mag star in V-Band.
62 |
63 | See Also
64 | --------
65 | SolarY.instruments.optics.Reflector
66 | SolarY.instruments.camera.CCD
67 | """
68 |
69 | def __init__(self, optics: Reflector, ccd: CCD) -> None:
70 | """
71 | Init function.
72 |
73 | Parameters
74 | ----------
75 | optics : Reflector
76 | Reflector object.
77 | ccd : CCD
78 | CCD object.
79 |
80 | See Also
81 | --------
82 | SolarY.instruments.optics.Reflector :
83 | The Reflector base class that contains the optics attributes and properties.
84 | SolarY.instruments.camera.CCD :
85 | The CCD base class that contains the camera attributes and properties.
86 | """
87 | # # Init the optics and camera classes accordingly
88 | # Reflector.__init__(self, **optics_config)
89 | # CCD.__init__(self, **ccd_config)
90 | self._ccd = ccd
91 | self._optics = optics
92 |
93 | # Load the constants config file and get the photon flux (Given in m^-2 * s^-1)
94 | config = solary_auxiliary.config.get_constants()
95 | self._photon_flux_v = float(config["photometry"]["photon_flux_V"])
96 | self._aperture = 0.0 # TODO: this should be passed in as an argument
97 | self._hfdia = 0.0 # TODO: this should be passed in as an argument
98 | self._exposure_time = 0.0 # TODO: this should be passed in as an argument
99 |
100 | @classmethod
101 | def load_from_json_files(
102 | cls, optics_path: t.Union[Path, str], ccd_path: t.Union[Path, str]
103 | ) -> "ReflectorCCD":
104 | """Construct a ReflectorCCD object JSON files."""
105 | ccd = CCD.load_from_json_file(ccd_path)
106 | optics = Reflector.load_from_json_file(optics_path)
107 |
108 | return ReflectorCCD(optics=optics, ccd=ccd)
109 |
110 | @property
111 | def ccd(self) -> CCD:
112 | """Get the CCD object instance."""
113 | return self._ccd
114 |
115 | @property
116 | def optics(self) -> Reflector:
117 | """Get the Reflector optics object instance."""
118 | return self._optics
119 |
120 | @property
121 | def fov(self) -> t.Tuple[float, float]:
122 | """
123 | Get the Field-Of-View (FOV) of the telescope in x and y dimensions.
124 |
125 | Returns
126 | -------
127 | fov_res : list
128 | FOV values (x and y dimension of the CCD chip). Given in arcsec.
129 | """
130 | # Placholder list for the results
131 | fov_res = []
132 |
133 | # Iterate through the chip size list (given in mm) and multiply the focal length that is
134 | # given in meters with 1000 to convert it to mm.
135 | for chip_dim in self.ccd.chip_size:
136 | fov_res.append(
137 | comp_fov(
138 | sensor_dim=chip_dim, focal_length=self.optics.focal_length * 1000.0
139 | )
140 | )
141 |
142 | return fov_res[0], fov_res[1]
143 |
144 | @property
145 | def ifov(self) -> t.Tuple[float, float]:
146 | """
147 | Get the individual Field-Of-View (iFOV). The iFOV is the FOV that applies for each pixel.
148 |
149 | Returns
150 | -------
151 | ifov_res : list
152 | iFOV values (x and y dimension of the CCD chip). Given in arcsec / pixel.
153 | """
154 | # Placeholder list for the iFOV results
155 | ifov_res = []
156 |
157 | # Iterate through the FOV values and number of pixels. Divide the FOV dimension by the
158 | # corresponding number of pixels
159 | for fov_dim, pixel_dim in zip(self.fov, self.ccd.pixels):
160 | ifov_res.append(fov_dim / pixel_dim)
161 |
162 | return ifov_res[0], ifov_res[1]
163 |
164 | @property
165 | def aperture(self) -> float:
166 | """
167 | Get the aperture.
168 |
169 | Returns
170 | -------
171 | float
172 | Photometry aperture. Given in arcsec.
173 | """
174 | return self._aperture
175 |
176 | @aperture.setter
177 | def aperture(self, apert: float) -> None:
178 | """
179 | Set the aperture.
180 |
181 | Parameters
182 | ----------
183 | apert : float
184 | Aperture for photometric / astrometric purposes. Given in arcsec.
185 |
186 | Returns
187 | -------
188 | None.
189 | """
190 | self._aperture = apert
191 |
192 | @property
193 | def hfdia(self) -> float:
194 | """
195 | Get the half flux diameter of an object / star.
196 |
197 | Returns
198 | -------
199 | float
200 | Half Flux Diameter. Given in arcsec.
201 | """
202 | return self._hfdia
203 |
204 | @hfdia.setter
205 | def hfdia(self, halfflux_dia: float) -> None:
206 | """
207 | Set the half flux diameter of an object / star.
208 |
209 | Parameters
210 | ----------
211 | halfflux_dia : float
212 | Half Flux Diameter. Given in arcsec.
213 |
214 | Returns
215 | -------
216 | None.
217 | """
218 | self._hfdia = halfflux_dia
219 |
220 | @property
221 | def exposure_time(self) -> float:
222 | """
223 | Get the exposure time.
224 |
225 | Returns
226 | -------
227 | float
228 | Exposure time. Given in s.
229 | """
230 | return self._exposure_time
231 |
232 | @exposure_time.setter
233 | def exposure_time(self, exp_time: float) -> None:
234 | """
235 | Set the exposure time.
236 |
237 | Parameters
238 | ----------
239 | exp_time : float
240 | Exposure time. Given in s.
241 |
242 | Returns
243 | -------
244 | None.
245 | """
246 | self._exposure_time = exp_time
247 |
248 | @property
249 | def pixels_in_aperture(self) -> int:
250 | """
251 | Get the number of pixels within the photometric aperture.
252 |
253 | Returns
254 | -------
255 | pixels_in_aperture : int
256 | Number of pixels within the aperture (rounded).
257 | """
258 | # Number of pixels corresponds to the aperture area (assuming a cirlce) divided by the iFOV
259 | frac_pixels_in_aperture = solary_general.geometry.circle_area(
260 | 0.5 * self.aperture
261 | ) / math.prod(self.ifov)
262 |
263 | # Round the result
264 | pixels_in_aperture = int(round(frac_pixels_in_aperture, 0))
265 |
266 | return pixels_in_aperture
267 |
268 | @property
269 | def _ratio_light_aperture(self) -> float:
270 | """
271 | Get the ratio of light that is collected within the photometric aperture.
272 |
273 | Assumption: the light of an object is Gaussian distributed (in both directions equally);
274 | the ration within the photometric aperture can be perfectly described by the
275 | error functon.
276 |
277 | Returns
278 | -------
279 | _ratio : float
280 | Ratio of light within the photometric aperture. Value between 0 and 1.
281 | """
282 | # Compute the Gaussian standard deviation of the half flux diameter in arcsec
283 | sigma = solary_general.geometry.fwhm2std(self.hfdia)
284 |
285 | # Compute the ratio, using the error function, the photometric aperture and half flux
286 | # diameter corresponding standard deviation
287 | _ratio = math.erf(self.aperture / (sigma * math.sqrt(2))) ** 2.0
288 |
289 | return _ratio
290 |
291 | def object_esignal(self, mag: float) -> float:
292 | """Return the object's signal in electrons.
293 |
294 | This function compute the number of the object's corresponding electrons that are
295 | created within the photometric aperture on the CCD.
296 |
297 | Parameters
298 | ----------
299 | mag : float
300 | Brightness of the object in V-Band. Given in mag.
301 |
302 | Returns
303 | -------
304 | obj_sig_aper : float
305 | Number of electrons that are created within the aperture.
306 | """
307 | # Compute the number of electrons:
308 | # 1. Scale by the magnitude
309 | # 2. Multiply by the photon flux in V-band
310 | # 3. Multiply by the exposure time in seconds
311 | # 4. Multiply by the telescope's collection area
312 | # 5. Multiply by the light ratio within the aperture
313 | # 6. Multiply by the quantum efficiency
314 | # 7. Multiply by the optical throughput
315 | obj_sig_aper = (
316 | 10.0 ** (-0.4 * mag)
317 | * self._photon_flux_v
318 | * self.exposure_time
319 | * self.optics.collect_area
320 | * self._ratio_light_aperture
321 | * self.ccd.quantum_eff
322 | * self.optics.optical_throughput
323 | )
324 |
325 | # Round the result
326 | obj_sig_aper = round(obj_sig_aper, 0)
327 |
328 | return obj_sig_aper
329 |
330 | def sky_esignal(self, mag_arcsec_sq: float) -> float:
331 | """Return the sky brightness signal in electrons.
332 |
333 | Compute the number of electrons within the photometric aperture
334 | caused by the background sky brightness.
335 |
336 | Parameters
337 | ----------
338 | mag_arcsec_sq : float
339 | Background sky brightness in V-Band. Given in mag/arcsec^2
340 |
341 | Returns
342 | -------
343 | sky_sig_aper : float
344 | Number of electrons that are created within the aperture.
345 | """
346 | # First, convert the sky surface brightness to an integrated
347 | # brightness (apply the complete telescope's collection area)
348 | total_sky_mag = solary_general.photometry.surmag2intmag(
349 | surmag=mag_arcsec_sq, area=math.prod(self.fov)
350 | )
351 | # Compute the number of electrons:
352 | # 1. Scale by the magnitude
353 | # 2. Multiply by the photon flux in V-band
354 | # 3. Multiply by the exposure time in seconds
355 | # 4. Multiply by the telescope's collection area
356 | # 5. Multiply by the number of pixels within the aperture w.r.t. the total number of
357 | # pixels (discrete ratio)
358 | # 6. Multiply by the quantum efficiency
359 | # 7. Multiply by the optical throughput
360 | sky_sig_aper = (
361 | 10.0 ** (-0.4 * total_sky_mag)
362 | * self._photon_flux_v
363 | * self.exposure_time
364 | * self.optics.collect_area
365 | * (self.pixels_in_aperture / math.prod(self.ccd.pixels))
366 | * self.ccd.quantum_eff
367 | * self.optics.optical_throughput
368 | )
369 |
370 | # Round the result
371 | sky_sig_aper = round(sky_sig_aper, 0)
372 |
373 | return sky_sig_aper
374 |
375 | @property
376 | def dark_esignal_aperture(self) -> float:
377 | """
378 | Get the number of dark current induced electrons within the aperture.
379 |
380 | Returns
381 | -------
382 | dark_sig_aper : float
383 | Number of dark current electrons.
384 | """
385 | # Compute the number of dark current electrons (Noise * exposure time * number of pixels
386 | # within the photometric aperture)
387 | dark_sig_aper = (
388 | self.ccd.dark_noise * self.exposure_time * self.pixels_in_aperture
389 | )
390 |
391 | # Round the result
392 | dark_sig_aper = round(dark_sig_aper, 0)
393 |
394 | return dark_sig_aper
395 |
396 | def object_snr(self, obj_mag: float, sky_mag_arcsec_sq: float) -> float:
397 | """
398 | Compute the Signal-To-Noise ratio (SNR) of an object.
399 |
400 | Parameters
401 | ----------
402 | obj_mag : float
403 | Object brightness. Given in mag.
404 | sky_mag_arcsec_sq : float
405 | Background sky brightness. Given in mag/arcsec^2.
406 |
407 | Returns
408 | -------
409 | snr : float
410 | SNR of the object.
411 | """
412 | # Compute the signal of the object (electrons within the photometric aperture)
413 | signal = self.object_esignal(mag=obj_mag)
414 |
415 | # Compute the noise
416 | noise = math.sqrt(
417 | signal
418 | + self.sky_esignal(mag_arcsec_sq=sky_mag_arcsec_sq)
419 | + self.dark_esignal_aperture
420 | )
421 |
422 | # Determine the SNR
423 | snr = signal / noise
424 |
425 | return snr
426 |
--------------------------------------------------------------------------------
/SolarY/neo/__init__.py:
--------------------------------------------------------------------------------
1 | """Submodule contains Near-Earth Objects (NEOs) related topics."""
2 | # flake8: noqa
3 | from . import astrodyn
4 | from . import data
5 |
--------------------------------------------------------------------------------
/SolarY/neo/astrodyn.py:
--------------------------------------------------------------------------------
1 | """NEO related astro-dynamical functions and classes."""
2 |
3 |
4 | def neo_class(sem_maj_axis_au: float,
5 | peri_helio_au: float,
6 | ap_helio_au: float) -> str:
7 | """Classify the NEO based on the orbital parameters.
8 |
9 | Depending on the semi-major axis, perihelion and / or aphelion a NEO can be classified as an
10 | Amor, Apollo, Aten or Atira.
11 |
12 | Parameters
13 | ----------
14 | sem_maj_axis_au : float
15 | Semi-major axis of the NEO. Given in AU
16 | peri_helio_au : float
17 | Perihelion of the NEO. Given in AU
18 | ap_helio_au : float
19 | Aphelion of the NEO. Given in AU
20 |
21 | Returns
22 | -------
23 | neo_type : str
24 | NEO class / type.
25 |
26 | References
27 | ----------
28 | -1- Link to the NEO classifiction schema: https://cneos.jpl.nasa.gov/about/neo_groups.html
29 | """
30 | # Determine the NEO class in an extensive if-else statement
31 | if (sem_maj_axis_au > 1.0) & (1.017 < peri_helio_au < 1.3):
32 | neo_type = 'Amor'
33 |
34 | elif (sem_maj_axis_au > 1.0) & (peri_helio_au < 1.017):
35 | neo_type = 'Apollo'
36 |
37 | elif (sem_maj_axis_au < 1.0) & (ap_helio_au > 0.983):
38 | neo_type = 'Aten'
39 |
40 | elif (sem_maj_axis_au < 1.0) & (ap_helio_au < 0.983):
41 | neo_type = 'Atira'
42 |
43 | else:
44 | neo_type = 'Other'
45 |
46 | return neo_type
47 |
--------------------------------------------------------------------------------
/SolarY/neo/data.py:
--------------------------------------------------------------------------------
1 | """NEO data download, parsing and database creation functions are part of this sub-module."""
2 | import gzip
3 | import os
4 | import re
5 | import shutil
6 | import sqlite3
7 | import time
8 | import typing as t
9 | from pathlib import Path
10 |
11 | import requests
12 |
13 | from .. import auxiliary as solary_auxiliary
14 | from .. import general as solary_general
15 | from . import astrodyn
16 |
17 | # Get the file paths
18 | PATH_CONFIG = solary_auxiliary.config.get_paths()
19 |
20 |
21 | def _get_neodys_neo_nr() -> int:
22 | """
23 | Get the number of currently known NEOs from the NEODyS webpage.
24 |
25 | The information is obtained by a Crawler-like script that may need frequent
26 | maintenance.
27 |
28 | Returns
29 | -------
30 | neodys_nr_neos : int
31 | Number of catalogued NEOs in the NEODyS database.
32 | """
33 | # Open the NEODyS link, where the current number of NEOs is shown
34 | # http_response = urllib.request.urlopen('https://newton.spacedys.com/neodys/index.php?pc=1.0')
35 |
36 | # Get the HTML response and read its content
37 | # html_content = http_response.read()
38 |
39 | http_response = requests.get("https://newton.spacedys.com/neodys/index.php?pc=1.0")
40 | html_content = http_response.content
41 |
42 | # Extract the number of NEOs from a specific HTML position, using a regular expression. The
43 | # number is displayed in bold like "[...] 1000 objects in the NEODys [...]"
44 | neodys_nr_neos = int(
45 | re.findall(r"(.*?) objects in the NEODyS", str(html_content))[0]
46 | )
47 |
48 | return neodys_nr_neos
49 |
50 |
51 | def download(row_exp: t.Optional[bool] = None) -> t.Tuple[str, t.Optional[int]]:
52 | """
53 | Download the orbital elements of all known NEOs.
54 |
55 | This function downloads a file with the orbital elements of currently all known NEOs from the
56 | NEODyS database. The file has the ending .cat and is basically a csv / ascii formatted file
57 | that can be read by any editor. See -1-
58 |
59 | Parameters
60 | ----------
61 | row_exp : bool, optional
62 | Boolean value. If the input is set to True the number of NEOs that are listed in the
63 | downloaded file are compared with the number of expected NEOs (number of NEOs listed on the
64 | NEODyS page). The default is None.
65 |
66 | Returns
67 | -------
68 | dl_status : str
69 | Human-readable status report that returns 'OK' or 'ERROR', depending on the download's
70 | success.
71 | neodys_neo_nr : int
72 | Number of NEOs (from the NEODyS page and thus optional). This value can be compared with
73 | the content of the downloaded file to determine whether entries are missing or not. Per
74 | default None is returned.
75 |
76 | References
77 | ----------
78 | -1- Link to the NEODyS data: https://newton.spacedys.com/neodys/index.php?pc=1.0
79 | """
80 | # Set the complete filepath. The file is stored in the user's home directory
81 | download_filename = solary_auxiliary.parse.setnget_file_path(
82 | PATH_CONFIG["neo"]["neodys_raw_dir"], PATH_CONFIG["neo"]["neodys_raw_file"]
83 | )
84 |
85 | # Download the file
86 | response = requests.get("https://newton.spacedys.com/~neodys2/neodys.cat")
87 | download_file_path = Path(download_filename)
88 | with download_file_path.open(mode="wb+") as file_obj:
89 | file_obj.write(response.content)
90 |
91 | # downl_file_path, _ = \
92 | # urllib.request.urlretrieve(url='https://newton.spacedys.com/~neodys2/neodys.cat', \
93 | # filename=download_filename)
94 |
95 | # To check whether the file has been successfully updated (if a file was present before) one
96 | # needs to compare the "last modification time" of the file with the current system's time. If
97 | # the deviation is too high, it is very likely that no new file has been downloaded and
98 | # consequently updated
99 | system_time = time.time()
100 | # file_mod_time = os.path.getmtime(downl_file_path)
101 | file_mod_time = download_file_path.stat().st_mtime
102 | file_mod_diff = file_mod_time - system_time
103 |
104 | # Set status message, if the file has been updated or not. A time difference of less than 5 s
105 | # shall indicate whether the file is new or not
106 | if file_mod_diff < 5:
107 | dl_status = "OK"
108 | else:
109 | dl_status = "ERROR"
110 |
111 | # Optional: Get the number of expected NEOs from the NEODyS webpage
112 | neodys_neo_nr = None
113 | if row_exp:
114 | neodys_neo_nr = _get_neodys_neo_nr()
115 |
116 | return dl_status, neodys_neo_nr
117 |
118 |
119 | def read_neodys() -> t.List[t.Dict[str, t.Any]]:
120 | """
121 | Read the content of the downloaded NEODyS file and return a dictionary with its content.
122 |
123 | Returns
124 | -------
125 | neo_dict : list
126 | List of dictionaries that contains the NEO data from the NEODyS download.
127 | """
128 | # Set the download file path. The file shall be stored in the home direoctry
129 | path_filename = solary_auxiliary.parse.setnget_file_path(
130 | PATH_CONFIG["neo"]["neodys_raw_dir"], PATH_CONFIG["neo"]["neodys_raw_file"]
131 | )
132 |
133 | # Set a placeholder dictionary where the data will be stored
134 | neo_dict = []
135 |
136 | # Open the NEODyS file. Ignore the header (first 6 rows) and iterate through the file row-wise.
137 | # Read the content adn save it in the dictionary
138 | with open(path_filename) as f_temp:
139 | neo_data = f_temp.readlines()[6:]
140 | for neo_data_line_f in neo_data:
141 | neo_data_line = neo_data_line_f.split()
142 | neo_dict.append(
143 | {
144 | "Name": neo_data_line[0].replace("'", ""),
145 | "Epoch_MJD": float(neo_data_line[1]),
146 | "SemMajAxis_AU": float(neo_data_line[2]),
147 | "Ecc_": float(neo_data_line[3]),
148 | "Incl_deg": float(neo_data_line[4]),
149 | "LongAscNode_deg": float(neo_data_line[5]),
150 | "ArgP_deg": float(neo_data_line[6]),
151 | "MeanAnom_deg": float(neo_data_line[7]),
152 | "AbsMag_": float(neo_data_line[8]),
153 | "SlopeParamG_": float(neo_data_line[9]),
154 | }
155 | )
156 |
157 | return neo_dict
158 |
159 |
160 | class NEOdysDatabase:
161 | """Class to create, update and read an SQLite based database.
162 |
163 | Class to create, update and read an SQLite based database that contains NEO data (raw and
164 | derived parameters) based on the NEODyS data.
165 |
166 | Attributes
167 | ----------
168 | db_filename : str
169 | Absolute path to the SQLite NEODyS database
170 | con : sqlite3.Connection
171 | Connection to the SQLite NEODyS database
172 | cur: sqlite3.Cursor
173 | Cursor to the SQLite NEODyS database
174 |
175 | Methods
176 | -------
177 | __init__(new=False)
178 | Init function at the class call. Allows one to re-create a new SQLite database from
179 | scratch.
180 | create()
181 | Create the main table of the SQLite NEODyS database (contains only the raw input data, no
182 | derived parameters).
183 | create_deriv_orb()
184 | Compute derived orbital elements from the raw input data.
185 | close()
186 | Close the SQLite database.
187 |
188 | See Also
189 | --------
190 | SolarY.neo.data.download
191 | SolarY.neo.data.read_neodys
192 | """
193 |
194 | def __init__(self, new: bool = False) -> None:
195 | """
196 | Initialize the NEODySDatabase class.
197 |
198 | This method creates a new database or opens an existing one (if
199 | applicable) and sets a cursor.
200 |
201 | Parameters
202 | ----------
203 | new : bool, optional
204 | If True: a new database will be created from scratch. WARNING: this will delete any
205 | previously built SQLite database with the name "neo_neodys.db" in the home directory.
206 | The default is False.
207 | """
208 | # Set / Get an SQLite database path + filename
209 | self.db_filename = solary_auxiliary.parse.setnget_file_path(
210 | PATH_CONFIG["neo"]["neodys_db_dir"], PATH_CONFIG["neo"]["neodys_db_file"]
211 | )
212 |
213 | # Delete any existing database, if requested
214 | if new and os.path.exists(self.db_filename):
215 | os.remove(self.db_filename)
216 |
217 | # Connect / Build database and set a cursor
218 | self.con = sqlite3.connect(self.db_filename)
219 | self.cur = self.con.cursor()
220 |
221 | def _create_col(self, table: str, col_name: str, col_type: str) -> None:
222 | """
223 | Private method to create new columns in tables.
224 |
225 | Parameters
226 | ----------
227 | table : str
228 | Table name, where a new column shall be added.
229 | col_name : str
230 | Column name.
231 | col_type : str
232 | SQLite column type (FLOAT, INT, TEXT, etc.).
233 | """
234 | # Generic f-string that represents an SQLite command to alter a table (adding a new column
235 | # with its dtype).
236 | sql_col_create = f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}"
237 |
238 | # Try to create a new column. If is exists an sqlite3.OperationalError weill raise. Pass
239 | # this error.
240 | # TODO: change the error passing
241 | try:
242 | self.cur.execute(sql_col_create)
243 | self.con.commit()
244 | except sqlite3.OperationalError:
245 | pass
246 |
247 | def create(self) -> None:
248 | """
249 | Create the NEODyS main table.
250 |
251 | Method to create the NEODyS main table, read the downloaded content and fill the database
252 | with the raw data.
253 | """
254 | # Create the main table
255 | self.cur.execute(
256 | "CREATE TABLE IF NOT EXISTS main(Name TEXT PRIMARY KEY, "
257 | "Epoch_MJD FLOAT, "
258 | "SemMajAxis_AU FLOAT, "
259 | "Ecc_ FLOAT, "
260 | "Incl_deg FLOAT, "
261 | "LongAscNode_deg FLOAT, "
262 | "ArgP_deg FLOAT, "
263 | "MeanAnom_deg FLOAT, "
264 | "AbsMag_ FLOAT, "
265 | "SlopeParamG_ FLOAT)"
266 | )
267 | self.con.commit()
268 |
269 | # Read the NEODyS raw data
270 | _neo_data = read_neodys()
271 |
272 | # Insert the raw data into the database
273 | self.cur.executemany(
274 | "INSERT OR IGNORE INTO main(Name, "
275 | "Epoch_MJD, "
276 | "SemMajAxis_AU, "
277 | "Ecc_, "
278 | "Incl_deg, "
279 | "LongAscNode_deg, "
280 | "ArgP_deg, "
281 | "MeanAnom_deg, "
282 | "AbsMag_, "
283 | "SlopeParamG_) "
284 | "VALUES (:Name, "
285 | ":Epoch_MJD, "
286 | ":SemMajAxis_AU, "
287 | ":Ecc_, "
288 | ":Incl_deg, "
289 | ":LongAscNode_deg, "
290 | ":ArgP_deg, "
291 | ":MeanAnom_deg, "
292 | ":AbsMag_, "
293 | ":SlopeParamG_)",
294 | _neo_data,
295 | )
296 | self.con.commit()
297 |
298 | def create_deriv_orb(self) -> None:
299 | """Compute and insert derived orbital elements into the SQLite database."""
300 | # Add new columns in the main table
301 | self._create_col("main", "Aphel_AU", "FLOAT")
302 | self._create_col("main", "Perihel_AU", "FLOAT")
303 |
304 | # Get orbital elements to compute the derived parameters
305 | self.cur.execute("SELECT Name, SemMajAxis_AU, Ecc_ FROM main")
306 |
307 | # Fetch the data
308 | _neo_data = self.cur.fetchall()
309 |
310 | # Iterate throuh the results, compute the derived elements and put them in a list of
311 | # dicitionaries
312 | _neo_deriv_param_dict = []
313 | for _neo_data_line_f in _neo_data:
314 | _neo_deriv_param_dict.append(
315 | {
316 | "Name": _neo_data_line_f[0],
317 | "Aphel_AU": solary_general.astrodyn.kep_apoapsis(
318 | sem_maj_axis=_neo_data_line_f[1], ecc=_neo_data_line_f[2]
319 | ),
320 | "Perihel_AU": solary_general.astrodyn.kep_periapsis(
321 | sem_maj_axis=_neo_data_line_f[1], ecc=_neo_data_line_f[2]
322 | ),
323 | }
324 | )
325 |
326 | # Insert the data into the main table
327 | self.cur.executemany(
328 | "UPDATE main SET Aphel_AU = :Aphel_AU, Perihel_AU = :Perihel_AU "
329 | "WHERE Name = :Name",
330 | _neo_deriv_param_dict,
331 | )
332 | self.con.commit()
333 |
334 | def create_neo_class(self) -> None:
335 | """Compute and insert the NEO classification into the database."""
336 | # Add a new column in the main table
337 | self._create_col(table="main", col_name="NEOClass", col_type="Text")
338 |
339 | # Get the orbital elements to compute the NEO class
340 | self.cur.execute("SELECT Name, SemMajAxis_AU, Perihel_AU, Aphel_AU FROM main")
341 |
342 | # Fetch the data
343 | _neo_data = self.cur.fetchall()
344 |
345 | # Iterate throuh the results, compute the NEO class and put in into the results
346 | _neo_class_param_dict = []
347 | for _neo_data_line_f in _neo_data:
348 | _neo_class_param_dict.append(
349 | {
350 | "Name": _neo_data_line_f[0],
351 | "NEOClass": astrodyn.neo_class(
352 | sem_maj_axis_au=_neo_data_line_f[1],
353 | peri_helio_au=_neo_data_line_f[2],
354 | ap_helio_au=_neo_data_line_f[3]
355 | )
356 | }
357 | )
358 |
359 | # Insert the data into the main table
360 | self.cur.executemany(
361 | "UPDATE main SET NEOClass = :NEOClass "
362 | "WHERE Name = :Name",
363 | _neo_class_param_dict,
364 | )
365 | self.con.commit()
366 |
367 | def update(self) -> None:
368 | """Update the NEODyS Database with all content."""
369 | # Call the create functions that insert new data
370 | self.create()
371 | self.create_deriv_orb()
372 | self.create_neo_class()
373 |
374 | def close(self) -> None:
375 | """Close the SQLite NEODyS database."""
376 | self.con.close()
377 |
378 |
379 | def download_granvik2018() -> str:
380 | """
381 | Download the model data from Granvik et al. (2018) -1-.
382 |
383 | The data can be found in -2-.
384 |
385 | Returns
386 | -------
387 | sha256_hash : str
388 | SHA256 hash of the downloaded file.
389 |
390 | References
391 | ----------
392 | 1. Granvik, Morbidelli, Jedicke, Bolin, Bottke, Beshore, Vokrouhlicky, Nesvorny, and Michel
393 | (2018). Debiased orbit and absolute-magnitude distributions for near-Earth objects.
394 | Accepted for publication in Icarus.
395 | 2. https://www.mv.helsinki.fi/home/mgranvik/data/Granvik+_2018_Icarus/
396 | """
397 | # Set the download path to the home directory
398 | download_filename = solary_auxiliary.parse.setnget_file_path(
399 | PATH_CONFIG["neo"]["granvik2018_raw_dir"],
400 | PATH_CONFIG["neo"]["granvik2018_raw_file"],
401 | )
402 |
403 | # Set the downlaod URL
404 | url_location = (
405 | "https://www.mv.helsinki.fi/home/mgranvik/data/"
406 | "Granvik+_2018_Icarus/Granvik+_2018_Icarus.dat.gz"
407 | )
408 |
409 | # Retrieve the data (download)
410 | response = requests.get(url_location)
411 | downl_file_path = Path(download_filename)
412 | with downl_file_path.open(mode="wb+") as file_obj:
413 | file_obj.write(response.content)
414 |
415 | # downl_file_path, _ = urllib.request.urlretrieve(url=url_location, \
416 | # filename=download_filename)
417 |
418 | # Get the file name (without the gzip ending). Open the gzip file and move the .dat file out.
419 | # unzip_file_path = downl_file_path[:-3]
420 | unzip_file_path = downl_file_path.with_suffix("")
421 | with gzip.open(downl_file_path, "r") as f_in, open(unzip_file_path, "wb") as f_out:
422 | shutil.copyfileobj(f_in, f_out)
423 |
424 | # Delete the gzip file
425 | os.remove(downl_file_path)
426 |
427 | # Compute the SHA256 hash
428 | sha256_hash = solary_auxiliary.parse.comp_sha256(unzip_file_path)
429 |
430 | return sha256_hash
431 |
432 |
433 | def read_granvik2018() -> t.List[t.Dict[str, t.Any]]:
434 | """
435 | Read the content of the downloaded orbital elements file.
436 |
437 | Read the content of the downloaded Granvik et al. (2018) NEO model data file and return a
438 | dictionary with its content.
439 |
440 | Returns
441 | -------
442 | neo_dict : list
443 | List of dictionaries that contains the NEO data from the downloaded model data.
444 | """
445 | # Set the download path of the model file
446 | path_filename = solary_auxiliary.parse.setnget_file_path(
447 | PATH_CONFIG["neo"]["granvik2018_raw_dir"],
448 | PATH_CONFIG["neo"]["granvik2018_unzip_file"],
449 | )
450 |
451 | # Iterate through the downloaded file and write the content in a list of dictionaries. Each
452 | # dictionary contains an individual simulated NEO
453 | neo_dict = []
454 | with open(path_filename) as f_temp:
455 | neo_data = f_temp.readlines()
456 |
457 | for neo_data_line_f in neo_data:
458 | neo_data_line = neo_data_line_f.split()
459 | neo_dict.append(
460 | {
461 | "SemMajAxis_AU": float(neo_data_line[0]),
462 | "Ecc_": float(neo_data_line[1]),
463 | "Incl_deg": float(neo_data_line[2]),
464 | "LongAscNode_deg": float(neo_data_line[3]),
465 | "ArgP_deg": float(neo_data_line[4]),
466 | "MeanAnom_deg": float(neo_data_line[5]),
467 | "AbsMag_": float(neo_data_line[6]),
468 | }
469 | )
470 |
471 | return neo_dict
472 |
473 |
474 | class Granvik2018Database:
475 | """
476 | Class to create, update and read an SQLite based database.
477 |
478 | Class to create, update and read an SQLite based database that contains the Granvik et al.
479 | (2018) NEO model data (raw and derived parameters)
480 |
481 | Attributes
482 | ----------
483 | db_filename : str
484 | Absolute path to the SQLite Granvik et al. (2018) database
485 | con : sqlite3.Connection
486 | Connection to the SQLite Granvik et al. (2018) database
487 | cur: sqlite3.Cursor
488 | Cursor to the SQLite Granvik et al. (2018) database
489 |
490 | Methods
491 | -------
492 | __init__(new=False)
493 | Init function at the class call. Allows one to re-create a new SQLite database from
494 | scratch.
495 | create()
496 | Create the main table of the SQLite Granvik et al. (2018) database (contains only the raw
497 | input data, no derived parameters).
498 | create_deriv_orb()
499 | Compute derived orbital elements from the raw input data.
500 | close()
501 | Close the SQLite database.
502 |
503 | See Also
504 | --------
505 | SolarY.neo.data.download_granvik2018
506 | SolarY.neo.data.read_granvik2018
507 | """
508 |
509 | def __init__(self, new: bool = False):
510 | """
511 | Init. function of the Granvik2018Database class.
512 |
513 | This method creates a new database or opens an existing one (if applicable)
514 | and sets a cursor.
515 |
516 | Parameters
517 | ----------
518 | new : bool, optional
519 | If True: a new database will be created from scratch. WARNING: this will delete any
520 | previously built SQLite database with the name "neo_granvik2018.db" in the home
521 | directory. The default is False.
522 | """
523 | # Set the database path to the home directory
524 | self.db_filename = solary_auxiliary.parse.setnget_file_path(
525 | PATH_CONFIG["neo"]["granvik2018_db_dir"],
526 | PATH_CONFIG["neo"]["granvik2018_db_file"],
527 | )
528 |
529 | # Delete any existing database, if requested
530 | if new and os.path.exists(self.db_filename):
531 | os.remove(self.db_filename)
532 |
533 | # Establish a connection and set a cursor to the database
534 | self.con = sqlite3.connect(self.db_filename)
535 | self.cur = self.con.cursor()
536 |
537 | def _create_col(self, table: str, col_name: str, col_type: str) -> None:
538 | """
539 | Private method to create new columns in tables.
540 |
541 | Parameters
542 | ----------
543 | table : str
544 | Table name, where a new column shall be added.
545 | col_name : str
546 | Column name.
547 | col_type : str
548 | SQLite column type (FLOAT, INT, TEXT, etc.).
549 | """
550 | # Generic f-string that represents an SQLite command to alter a table (adding a new column
551 | # with its dtype).
552 | sql_col_create = f"ALTER TABLE {table} ADD COLUMN {col_name} {col_type}"
553 |
554 | # Try to create a new column. If is exists an sqlite3.OperationalError weill raise. Pass
555 | # this error.
556 | # TODO: change the error passing and merge with the same method in NEODySDatabase
557 | try:
558 | self.cur.execute(sql_col_create)
559 | self.con.commit()
560 | except sqlite3.OperationalError:
561 | pass
562 |
563 | def create(self) -> None:
564 | """Create the Granvik et al. (2018) main table.
565 |
566 | Method to create the Granvik et al. (2018) main table, read the downloaded content and fill
567 | the database with the raw data.
568 | """
569 | # Create main table for the raw data
570 | self.cur.execute(
571 | "CREATE TABLE IF NOT EXISTS main(ID INTEGER PRIMARY KEY, "
572 | "SemMajAxis_AU FLOAT, "
573 | "Ecc_ FLOAT, "
574 | "Incl_deg FLOAT, "
575 | "LongAscNode_deg FLOAT, "
576 | "ArgP_deg FLOAT, "
577 | "MeanAnom_deg FLOAT, "
578 | "AbsMag_ FLOAT)"
579 | )
580 | self.con.commit()
581 |
582 | # Read the Granvik et al. (2018) data
583 | _neo_data = read_granvik2018()
584 |
585 | # Insert the raw Granvik et al. (2018) data into the SQLite database
586 | self.cur.executemany(
587 | "INSERT OR IGNORE INTO main(SemMajAxis_AU, "
588 | "Ecc_, "
589 | "Incl_deg, "
590 | "LongAscNode_deg, "
591 | "ArgP_deg, "
592 | "MeanAnom_deg, "
593 | "AbsMag_) "
594 | "VALUES (:SemMajAxis_AU, "
595 | ":Ecc_, "
596 | ":Incl_deg, "
597 | ":LongAscNode_deg, "
598 | ":ArgP_deg, "
599 | ":MeanAnom_deg, "
600 | ":AbsMag_)",
601 | _neo_data,
602 | )
603 | self.con.commit()
604 |
605 | def create_deriv_orb(self) -> None:
606 | """Compute and insert derived orbital elements into the SQLite database."""
607 | # Create new columns
608 | self._create_col("main", "Aphel_AU", "FLOAT")
609 | self._create_col("main", "Perihel_AU", "FLOAT")
610 |
611 | # Get all relevant information from the database
612 | self.cur.execute("SELECT ID, SemMajAxis_AU, Ecc_ FROM main")
613 | _neo_data = self.cur.fetchall()
614 |
615 | # Iterate through the results and compute the derived orbital parameters
616 | _neo_deriv_param_dict = []
617 | for _neo_data_line_f in _neo_data:
618 | _neo_deriv_param_dict.append(
619 | {
620 | "ID": _neo_data_line_f[0],
621 | "Aphel_AU": solary_general.astrodyn.kep_apoapsis(
622 | sem_maj_axis=_neo_data_line_f[1], ecc=_neo_data_line_f[2]
623 | ),
624 | "Perihel_AU": solary_general.astrodyn.kep_periapsis(
625 | sem_maj_axis=_neo_data_line_f[1], ecc=_neo_data_line_f[2]
626 | ),
627 | }
628 | )
629 |
630 | # Insert the derived paramters into the database
631 | self.cur.executemany(
632 | "UPDATE main SET Aphel_AU = :Aphel_AU, Perihel_AU = :Perihel_AU "
633 | "WHERE ID = :ID",
634 | _neo_deriv_param_dict,
635 | )
636 | self.con.commit()
637 |
638 | def create_neo_class(self) -> None:
639 | """Compute and insert the NEO classification into the database."""
640 | # Add a new column in the main table
641 | self._create_col(table="main", col_name="NEOClass", col_type="Text")
642 |
643 | # Get the orbital elements to compute the NEO class
644 | self.cur.execute("SELECT ID, SemMajAxis_AU, Perihel_AU, Aphel_AU FROM main")
645 |
646 | # Fetch the data
647 | _neo_data = self.cur.fetchall()
648 |
649 | # Iterate throuh the results, compute the NEO class and put in into the results
650 | _neo_class_param_dict = []
651 | for _neo_data_line_f in _neo_data:
652 | _neo_class_param_dict.append(
653 | {
654 | "ID": _neo_data_line_f[0],
655 | "NEOClass": astrodyn.neo_class(
656 | sem_maj_axis_au=_neo_data_line_f[1],
657 | peri_helio_au=_neo_data_line_f[2],
658 | ap_helio_au=_neo_data_line_f[3]
659 | )
660 | }
661 | )
662 |
663 | # Insert the data into the main table
664 | self.cur.executemany(
665 | "UPDATE main SET NEOClass = :NEOClass "
666 | "WHERE ID = :ID",
667 | _neo_class_param_dict,
668 | )
669 | self.con.commit()
670 |
671 | def close(self) -> None:
672 | """Close the Granvik et al. (2018) database."""
673 | self.con.close()
674 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = visdcf
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/README.rst:
--------------------------------------------------------------------------------
1 | Building the docs
2 | =================
3 |
4 | First install Sphinx and the RTD theme:
5 |
6 | pip install -r requirements-docs.txt
7 |
8 | or update it if already installed:
9 |
10 | pip install --upgrade sphinx sphinx_rtd_theme
11 |
12 | Go to the doc folder:
13 |
14 | cd doc
15 |
16 | Then build the HTML documentation:
17 |
18 | make html
19 |
20 | and the man page:
21 |
22 | LC_ALL=C make man
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 | set SPHINXPROJ=visdcf
13 |
14 | if "%1" == "" goto help
15 |
16 | %SPHINXBUILD% >NUL 2>NUL
17 | if errorlevel 9009 (
18 | echo.
19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
20 | echo.installed, then set the SPHINXBUILD environment variable to point
21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
22 | echo.may add the Sphinx directory to PATH.
23 | echo.
24 | echo.If you don't have Sphinx installed, grab it from
25 | echo.http://sphinx-doc.org/
26 | exit /b 1
27 | )
28 |
29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
30 | goto end
31 |
32 | :help
33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
34 |
35 | :end
36 | popd
37 |
--------------------------------------------------------------------------------
/docs/requirements-docs.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx-rtd-theme
3 | sphinxcontrib-plantuml
4 | sphinxcontrib-mermaid
5 | # sphinxcontrib-napoleon
6 | sphinx-autodoc-typehints
7 | # sphinx-autodoc-napoleon-typehints
8 | numpydoc
9 |
--------------------------------------------------------------------------------
/docs/source/api.rst.old:
--------------------------------------------------------------------------------
1 | Package API
2 | ===========
3 |
4 | astroid package
5 | ---------------
6 |
7 | .. automodule:: solary.asteroid.physp
8 | :members:
9 |
10 |
11 | auxiliary package
12 | -----------------
13 |
14 | .. automodule:: solary.auxiliary.config
15 | :members:
16 |
17 |
18 | .. automodule:: solary.auxiliary.parse
19 | :members:
20 |
21 |
22 | .. automodule:: solary.auxiliary.reader
23 | :members:
24 |
25 |
26 | general package
27 | ---------------
28 |
29 | .. automodule:: solary.instruments
30 | :members:
31 |
32 |
33 | instruments package
34 | -------------------
35 |
36 |
37 | .. automodule:: solary.instruments.camera
38 | :members:
39 |
40 | .. automodule:: solary.instruments.optics
41 | :special-members:
42 | :exclude-members: __dict__, __weakref__
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | .. automodule:: solary.instruments.telescope
48 | :special-members:
49 | :exclude-members: __dict__, __weakref__
50 | :members:
51 | :undoc-members:
52 | :show-inheritance:
53 |
54 | neo package
55 | -----------
56 |
57 | .. autoclass:: solary.neo.data.Granvik2018Database
58 | :special-members:
59 | :exclude-members: __dict__, __weakref__
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 |
64 | .. autoclass:: solary.neo.data.NEOdysDatabase
65 | :special-members:
66 | :exclude-members: __dict__, __weakref__
67 | :undoc-members:
68 | :show-inheritance:
69 |
--------------------------------------------------------------------------------
/docs/source/api/asteroid/index.rst:
--------------------------------------------------------------------------------
1 | Asteroid
2 | ========
3 |
4 | Physp
5 | -----
6 |
7 | .. automodule:: SolarY.asteroid.physp
8 | :members:
9 | :special-members:
10 | :exclude-members: __dict__, __weakref__
11 |
--------------------------------------------------------------------------------
/docs/source/api/auxiliary/index.rst:
--------------------------------------------------------------------------------
1 | Auxiliary
2 | =========
3 |
4 | Config
5 | ------
6 |
7 | .. automodule:: SolarY.auxiliary.config
8 | :members:
9 | :special-members:
10 | :exclude-members: __dict__, __weakref__
11 |
12 |
13 | Download
14 | --------
15 |
16 | .. automodule:: SolarY.auxiliary.download
17 | :members:
18 | :special-members:
19 | :exclude-members: __dict__, __weakref__
20 |
21 |
22 | Parse
23 | -----
24 |
25 | .. automodule:: SolarY.auxiliary.parse
26 | :members:
27 | :special-members:
28 | :exclude-members: __dict__, __weakref__
29 |
30 | Reader
31 | ------
32 |
33 | .. automodule:: SolarY.auxiliary.reader
34 | :members:
35 | :special-members:
36 | :exclude-members: __dict__, __weakref__
37 |
--------------------------------------------------------------------------------
/docs/source/api/general/index.rst:
--------------------------------------------------------------------------------
1 | General
2 | =======
3 |
4 | Astrodyn
5 | --------
6 |
7 | .. automodule:: SolarY.general.astrodyn
8 | :members:
9 | :special-members:
10 | :exclude-members: __dict__, __weakref__
11 |
12 |
13 | Geometry
14 | --------
15 |
16 | .. automodule:: SolarY.general.geometry
17 | :members:
18 | :special-members:
19 | :exclude-members: __dict__, __weakref__
20 |
21 |
22 | Photometry
23 | ----------
24 |
25 | .. automodule:: SolarY.general.photometry
26 | :members:
27 | :special-members:
28 | :exclude-members: __dict__, __weakref__
29 |
30 | Vec
31 | ---
32 |
33 | .. automodule:: SolarY.general.vec
34 | :members:
35 | :special-members:
36 | :exclude-members: __dict__, __weakref__
37 |
--------------------------------------------------------------------------------
/docs/source/api/index.rst:
--------------------------------------------------------------------------------
1 | .. toctree::
2 | :caption: API
3 | :maxdepth: 4
4 | :hidden:
5 |
6 | asteroid/index.rst
7 | auxiliary/index.rst
8 | general/index.rst
9 | instruments/index.rst
10 | neo/index.rst
11 |
--------------------------------------------------------------------------------
/docs/source/api/instruments/index.rst:
--------------------------------------------------------------------------------
1 | Instruments
2 | ===========
3 |
4 | Camera
5 | ------
6 |
7 | .. automodule:: SolarY.instruments.camera
8 | :members:
9 | :special-members:
10 | :exclude-members: __dict__, __weakref__
11 |
12 |
13 | Optics
14 | ------
15 |
16 | .. automodule:: SolarY.instruments.optics
17 | :members:
18 | :special-members:
19 | :exclude-members: __dict__, __weakref__
20 |
21 |
22 | Telescope
23 | ---------
24 |
25 | .. automodule:: SolarY.instruments.telescope
26 | :members:
27 | :special-members:
28 | :exclude-members: __dict__, __weakref__
29 |
--------------------------------------------------------------------------------
/docs/source/api/neo/index.rst:
--------------------------------------------------------------------------------
1 | NEO
2 | ===
3 |
4 | Data
5 | ----
6 |
7 | .. automodule:: SolarY.neo.data
8 | :members:
9 | :special-members:
10 | :exclude-members: __dict__, __weakref__
11 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # esapy_rpc documentation build configuration file, created by
5 | # sphinx-quickstart on Tue Nov 21 09:37:42 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | # import os
21 | # import sys
22 | # sys.path.insert(0, os.path.abspath('.'))
23 |
24 | from pathlib import Path
25 |
26 | import sphinx_rtd_theme
27 | from setuptools.config import read_configuration
28 |
29 | numfig = True
30 | numfig_format = {
31 | "figure": "Figure %s",
32 | "table": "Table %s",
33 | "code-block": "Listing %s",
34 | "section": "Section %s",
35 | }
36 |
37 | # ROOT_DIR = path.dirname(path.dirname(path.dirname(path.abspath(__file__))))
38 | # PROJ_NAME = path.split(ROOT_DIR)[1]
39 | # SRC_DIR = ROOT_DIR # path.join(ROOT_DIR, PROJ_NAME)
40 | # print('Adding project source directory to PYTHON_PATH: %s' % SRC_DIR)
41 | # sys.path.append(SRC_DIR)
42 |
43 | print("Imported package: SolarY")
44 |
45 | plantuml = "java -jar /usr/local/plantuml/plantuml.jar"
46 |
47 | # Read 'setup.cfg' file
48 | parent_folder = Path(__file__).parent
49 | setup_cfg_filename = parent_folder.joinpath("../../setup.cfg").resolve(
50 | strict=True
51 | ) # type: Path
52 | metadata = read_configuration(setup_cfg_filename)["metadata"] # type: dict
53 |
54 |
55 | # -- General configuration ------------------------------------------------
56 |
57 | # If your documentation needs a minimal Sphinx version, state it here.
58 | #
59 | # needs_sphinx = '1.0'
60 |
61 | # Add any Sphinx extension module names here, as strings. They can be
62 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
63 | # ones.
64 | # extensions = ['sphinx.ext.autodoc']
65 | extensions = [
66 | "sphinx.ext.autodoc", # include documentation from docstrings
67 | # 'sphinx.ext.todo', # support for todo items (.. todo::)
68 | # 'sphinx.ext.coverage', # collect doc coverage stats
69 | # 'sphinx.ext.mathjax', # render math via Javascript
70 | # 'sphinx.ext.viewcode', # add links to highlighted source code
71 | # 'sphinx.ext.autosummary', # Generate autodoc summaries
72 | # 'sphinx.ext.autosectionlabel',
73 | "numpydoc",
74 | "sphinx_autodoc_typehints",
75 | # 'sphinx_autodoc_napoleon_typehints',
76 | # 'sphinx.ext.napoleon' # Support for NumPy and Google style docstrings
77 | # 'sphinxcontrib.plantuml',
78 | # 'sphinxcontrib.mermaid',
79 | # 'sphinx.ext.numfig',
80 | # 'rst2pdf.pdfbuilder',
81 | ]
82 | numpydoc_show_class_members = False
83 | # generate autosummary even if no references
84 | # autosummary_generate = True
85 | # autosummary_imported_members = True
86 |
87 | autosectionlabel_prefix_document = True
88 | set_type_checking_flag = True
89 | # typehints_fully_qualified = True
90 | # always_document_param_types = True
91 | # typehints_document_rtype = True
92 |
93 | # Add any paths that contain templates here, relative to this directory.
94 | templates_path = ["_templates"]
95 |
96 | # The suffix(es) of source filenames.
97 | # You can specify multiple suffix as a list of string:
98 | #
99 | # source_suffix = ['.rst', '.md']
100 | source_suffix = ".rst"
101 |
102 | # The master toctree document.
103 | master_doc = "index"
104 |
105 | # General information about the project.
106 | project = metadata["name"]
107 | author = metadata["author"]
108 | title = project.title() + " User Manual"
109 |
110 | # The version info for the project you're documenting, acts as replacement for
111 | # |version| and |release|, also used in various other places throughout the
112 | # built documents.
113 | #
114 | # The short X.Y version.
115 | version = metadata["version"]
116 | # The full version, including alpha/beta/rc tags.
117 | release = metadata["version"].split("+", 1)[0]
118 |
119 | # The language for content autogenerated by Sphinx. Refer to documentation
120 | # for a list of supported languages.
121 | #
122 | # This is also used if you do content translation via gettext catalogs.
123 | # Usually you set "language" from the command line for these cases.
124 | language = None
125 |
126 | # List of patterns, relative to source directory, that match files and
127 | # directories to ignore when looking for source files.
128 | # This patterns also effect to html_static_path and html_extra_path
129 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
130 |
131 | # The name of the Pygments (syntax highlighting) style to use.
132 | pygments_style = "sphinx"
133 |
134 | # If true, `todo` and `todoList` produce output, else they produce nothing.
135 | todo_include_todos = False
136 |
137 |
138 | # -- Options for HTML output ----------------------------------------------
139 |
140 | # The theme to use for HTML and HTML Help pages. See the documentation for
141 | # a list of builtin themes.
142 | #
143 | html_theme = "sphinx_rtd_theme" # 'alabaster'
144 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
145 |
146 | # Theme options are theme-specific and customize the look and feel of a theme
147 | # further. For a list of options available for each theme, see the
148 | # documentation.
149 | #
150 | # html_theme_options = {}
151 |
152 | # Add any paths that contain custom static files (such as style sheets) here,
153 | # relative to this directory. They are copied after the builtin static files,
154 | # so a file named "default.css" will overwrite the builtin "default.css".
155 | html_static_path = ["_static"]
156 |
157 | # Custom sidebar templates, must be a dictionary that maps document names
158 | # to template names.
159 | #
160 | # This is required for the alabaster theme
161 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
162 | html_sidebars = {
163 | "**": [
164 | "relations.html", # needs 'show_related': True theme option to display
165 | "searchbox.html",
166 | ]
167 | }
168 |
169 |
170 | # -- Options for HTMLHelp output ------------------------------------------
171 |
172 | # Output file base name for HTML help builder.
173 | htmlhelp_basename = metadata["name"] + "doc"
174 |
175 |
176 | # -- Options for LaTeX output ---------------------------------------------
177 |
178 | latex_elements = {
179 | # The paper size ('letterpaper' or 'a4paper').
180 | #
181 | # 'papersize': 'letterpaper',
182 | # The font size ('10pt', '11pt' or '12pt').
183 | #
184 | # 'pointsize': '10pt',
185 | # Additional stuff for the LaTeX preamble.
186 | #
187 | # 'preamble': '',
188 | # Latex figure (float) alignment
189 | #
190 | # 'figure_align': 'htbp',
191 | }
192 |
193 | # Grouping the document tree into LaTeX files. List of tuples
194 | # (source start file, target name, title,
195 | # author, documentclass [howto, manual, or own class]).
196 | latex_documents = [
197 | (master_doc, project + ".tex", title, author, "manual"),
198 | ]
199 |
200 |
201 | # -- Options for manual page output ---------------------------------------
202 |
203 | # One entry per manual page. List of tuples
204 | # (source start file, name, description, authors, manual section).
205 | man_pages = [(master_doc, project, title, [author], 1)]
206 |
207 |
208 | # -- Options for Texinfo output -------------------------------------------
209 |
210 | # Grouping the document tree into Texinfo files. List of tuples
211 | # (source start file, target name, title, author,
212 | # dir menu entry, description, category)
213 | texinfo_documents = [
214 | (
215 | master_doc,
216 | project,
217 | title,
218 | author,
219 | project,
220 | "One line description of project.",
221 | "Miscellaneous",
222 | ),
223 | ]
224 |
225 |
226 | # def setup(app):
227 | # app.add_stylesheet('custom.css')
228 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Scene Projector
3 | ===============
4 |
5 | Table of Contents
6 | -----------------
7 |
8 |
9 | .. toctree::
10 | :caption: API
11 | :maxdepth: 4
12 | :hidden:
13 |
14 | api/index.rst
15 |
16 |
17 | Indices and tables
18 | ==================
19 |
20 | * :ref:`genindex`
21 | * :ref:`modindex`
22 | * :ref:`search`
23 |
--------------------------------------------------------------------------------
/mypy.ini:
--------------------------------------------------------------------------------
1 | [mypy]
2 | ; added settings here...
3 | warn_unreachable = True
4 | warn_unused_configs = True
5 | disallow_untyped_calls = True
6 | disallow_untyped_defs = True
7 | disallow_incomplete_defs = True
8 | check_untyped_defs = True
9 | disallow_untyped_decorators = True
10 | no_implicit_optional = True
11 | warn_redundant_casts = True
12 | warn_unused_ignores = True
13 | ;warn_return_any = True
14 | no_implicit_reexport = True
15 |
16 | [mypy-SolarY.tests.*]
17 | ignore_errors = True
18 |
19 | [mypy-SolarY._version]
20 | ignore_errors = True
21 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | log_cli = true
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi
2 | requests
3 | pytest
4 | spiceypy
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | # See the docstring in versioneer.py for instructions. Note that you must
2 | # re-run 'versioneer.py setup' after changing this section, and commit the
3 | # resulting files.
4 |
5 | # Versioneer is for later. See: https://github.com/python-versioneer/python-versioneer
6 | [versioneer]
7 | VCS = git
8 | style = pep440
9 | versionfile_source = SolarY/_version.py
10 | versionfile_build = SolarY/_version.py
11 | tag_prefix =
12 | parentdir_prefix = SolarY
13 |
14 | [bdist_wheel]
15 | # This flag says that the code is written to work on both Python 2 and Python
16 | # 3. If at all possible, it is good practice to do this. If you cannot, you
17 | # will need to generate wheels for each Python version that you support.
18 | universal=1
19 |
20 | [metadata]
21 | # See https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files
22 | name = SolarY
23 | version = attr: SolarY.__version__
24 | author = attr: SolarY.__author__
25 | url = https://github.com/ThomasAlbin/SolarY
26 | description = A Space Science library for asteroid, comets and meteors.
27 | long_description = file: README.md
28 | long_description_content_type=text/markdown
29 | # author_email = SolarY.__email__
30 | project_urls =
31 | Source = https://github.com/ThomasAlbin/SolarY
32 | Tracker = https://github.com/ThomasAlbin/SolarY/issues
33 | license= MIT License
34 | keywords = asteroid, comets, meteors
35 |
36 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
37 | classifiers =
38 | Development Status :: 4 - Beta
39 | Intended Audience :: Developers
40 | Intended Audience :: End Users/Desktop
41 | Topic :: Software Development :: Build Tools
42 | License :: OSI Approved :: MIT License
43 | Programming Language :: Python :: 3
44 | Programming Language :: Python :: 3.7
45 | Programming Language :: Python :: 3.8
46 | Programming Language :: Python :: 3.9
47 |
48 | platforms = unix, linux, osx, win32
49 |
50 | [options]
51 | zip_safe = True
52 | include_package_data = True
53 | packages = find:
54 | setup_requires =
55 | wheel>=0.29.0
56 | setuptools>=30.3
57 | install_requires =
58 | certifi
59 | requests
60 | spiceypy
61 |
62 | python_requires = >=3.7
63 |
64 | # [options.extras_require]
65 | # some_name =
66 | # some_package
67 |
68 | [options.packages.find]
69 | exclude =
70 | contrib
71 | docs
72 | tests
73 | examples
74 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """A setuptools based setup module.
2 |
3 | See:
4 | https://packaging.python.org/en/latest/distributing.html
5 | https://github.com/pypa/sampleproject
6 | """
7 |
8 |
9 | # Always prefer setuptools over distutils
10 | from setuptools import setup
11 |
12 | setup(package_data={"SolarY": ["py.typed"]})
13 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_asteroid
2 | from . import test_auxiliary
3 | from . import test_general
4 | from . import test_neo
5 |
6 | from . import test_instruments
7 |
8 | from . import _resources
9 |
--------------------------------------------------------------------------------
/tests/_resources/_config/test_paths.ini:
--------------------------------------------------------------------------------
1 | [instruments_optics_reflector]
2 | properties = tests/_resources/instruments/optics_reflector.json
3 |
4 | [instruments_camera_ccd]
5 | properties = tests/_resources/instruments/camera_ccd.json
6 |
7 | [general_astrodyn]
8 | base_class_orbit = tests/_resources/general/astrodyn_orbit_base_class.json
9 |
--------------------------------------------------------------------------------
/tests/_resources/general/astrodyn_orbit_base_class.json:
--------------------------------------------------------------------------------
1 | {
2 | "values" : {
3 | "peri" : 1.133,
4 | "ecc" : 0.223,
5 | "incl" : 10.8,
6 | "long_asc_node" : 304.4,
7 | "arg_peri": 178.8
8 | },
9 | "units" : {
10 | "spatial" : "AU",
11 | "angle" : "deg"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/_resources/instruments/camera_ccd.json:
--------------------------------------------------------------------------------
1 | {
2 | "pixels" : [4096, 4112],
3 | "pixel_size" : 15.0,
4 | "dark_noise" : 2.0,
5 | "readout_noise" : 1.0,
6 | "full_well": 300000.0,
7 | "quantum_eff": 0.5
8 | }
9 |
--------------------------------------------------------------------------------
/tests/_resources/instruments/optics_reflector.json:
--------------------------------------------------------------------------------
1 | {
2 | "main_mirror_dia" : 1.0,
3 | "sec_mirror_dia" : 0.2,
4 | "optical_throughput" : 0.6,
5 | "focal_length" : 10.0
6 | }
7 |
--------------------------------------------------------------------------------
/tests/test_asteroid/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_physp
2 |
--------------------------------------------------------------------------------
/tests/test_asteroid/test_physp.py:
--------------------------------------------------------------------------------
1 | """
2 | test_physp.py
3 |
4 | Testing suite for SolarY/asteroid/physp
5 |
6 | """
7 | import pytest
8 |
9 | import SolarY.asteroid
10 |
11 |
12 | def test_ast_size():
13 | """
14 | Test function for the asteroid size computation function
15 |
16 | Returns
17 | -------
18 | None.
19 |
20 | """
21 |
22 | # Compute the radius of an asteroid (test 1) and compare it with the (approximated expectation)
23 | ast_radius1 = SolarY.asteroid.physp.ast_size(albedo=0.05, abs_mag=10.0)
24 | assert pytest.approx(ast_radius1) == 29.71734
25 |
26 | # Compute the radius of an asteroid (test 2) and compare it with the (approximated expectation)
27 | ast_radius2 = SolarY.asteroid.physp.ast_size(albedo=0.3, abs_mag=20.0)
28 | assert pytest.approx(ast_radius2) == 0.12132055
29 |
30 | # Compute the radius of an asteroid (test 3) and compare it with the (approximated expectation)
31 | ast_radius3 = SolarY.asteroid.physp.ast_size(albedo=0.15, abs_mag=26.0)
32 | assert pytest.approx(ast_radius3) == 0.010825535
33 |
--------------------------------------------------------------------------------
/tests/test_auxiliary/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_config
2 | from . import test_download
3 | from . import test_parse
4 | from . import test_reader
5 |
--------------------------------------------------------------------------------
/tests/test_auxiliary/test_config.py:
--------------------------------------------------------------------------------
1 | """
2 | test_config.py
3 |
4 | Testing suite for SolarY/auxiliary/config.py
5 |
6 | """
7 | import SolarY
8 |
9 |
10 | def test_get_constants():
11 | """
12 | Test function to check whether the constants file is read successfully
13 |
14 | Returns
15 | -------
16 | None.
17 |
18 | """
19 |
20 | # Call the constants get function
21 | constant_config = SolarY.auxiliary.config.get_constants()
22 |
23 | # If the reading was successful the config object shall have miscellaneous sections and
24 | # corresponding values. One of them is called "constants"
25 | constant_config_sections = constant_config.sections()
26 | assert "constants" in constant_config_sections
27 |
28 |
29 | def test_get_paths():
30 | """
31 | Test function to check whether the path config file is read.
32 |
33 | Returns
34 | -------
35 | None.
36 |
37 | """
38 |
39 | # Call the paths config file
40 | paths_config = SolarY.auxiliary.config.get_paths()
41 |
42 | # If the reading was successful the config object shall have miscellaneous sections and
43 | # corresponding values. One of them is called "neo"
44 | paths_config_sections = paths_config.sections()
45 | assert "neo" in paths_config_sections
46 |
47 | # Testing now the test config
48 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
49 |
50 | # If the reading was successful the config object shall have miscellaneous sections and
51 | # corresponding values. One of them contains "instruments_telescope_optical"
52 | test_paths_config_sections = test_paths_config.sections()
53 | assert "instruments_optics_reflector" in test_paths_config_sections
54 |
55 |
56 | def test_get_spice_kernels():
57 | """
58 | The test function to check the correct parsinf of the SPICE config file.
59 |
60 | Returns
61 | -------
62 | None.
63 |
64 | """
65 |
66 | # Call the paths config file
67 | paths_config = SolarY.auxiliary.config.get_spice_kernels(ktype="generic")
68 |
69 | # If the reading was successful the config object shall have miscellaneous sections and
70 | # corresponding values. One of them is called "leapseconds". Further, "file" shall always
71 | # be present in a section.
72 | paths_config_sections = paths_config.sections()
73 | assert "leapseconds" in paths_config_sections
74 | assert "file" in paths_config["leapseconds"].keys()
75 |
--------------------------------------------------------------------------------
/tests/test_auxiliary/test_download.py:
--------------------------------------------------------------------------------
1 | """
2 | test_download.py
3 |
4 | Testing suite for SolarY/auxiliary/download.py
5 |
6 | """
7 | import SolarY
8 |
9 |
10 | def test_spice_generic_kernels():
11 | """
12 | Test function for the download function spice_generic_kernels.
13 |
14 | Returns
15 | -------
16 | None.
17 |
18 | """
19 |
20 | # Load the generic kernels config file
21 | paths_config = SolarY.auxiliary.config.get_spice_kernels(ktype="generic")
22 |
23 | # Create a dictionary that will contain the filepath and the corresponding MD5 has values for
24 | # each SPICE kernel (expectation)
25 | exp_kernel_dict = {}
26 |
27 | # Iterate trough the config file. Each section is an individual kernel
28 | for kernel in paths_config.sections():
29 |
30 | # Create the absolute filepath for of each kernel
31 | _download_filename = SolarY.auxiliary.parse.setnget_file_path(
32 | paths_config[kernel]["dir"], paths_config[kernel]["file"]
33 | )
34 |
35 | # Assign the filepath as a dict key and set the SHA256 hash as the corresponding value
36 | exp_kernel_dict[_download_filename] = paths_config[kernel]["sha256"]
37 |
38 | # Execute the SPICE download function. The resulting dictionary contains the resulting
39 | # filepaths and SHA256 hashes that shall ...
40 | res_dl_kernel_dict = SolarY.auxiliary.download.spice_generic_kernels()
41 |
42 | # ... correspond with the expectations
43 | assert res_dl_kernel_dict == exp_kernel_dict
44 |
--------------------------------------------------------------------------------
/tests/test_auxiliary/test_parse.py:
--------------------------------------------------------------------------------
1 | """
2 | test_parse.py
3 |
4 | Testing suite for SolarY/auxiliary/parse.py
5 |
6 | """
7 | import os
8 |
9 | import SolarY
10 |
11 |
12 | def test_comp_md5():
13 | """
14 | Test function to check if the function comp_md5 computes the correct MD5 hash value of a mock
15 | up file.
16 |
17 | Returns
18 | -------
19 | None.
20 |
21 | """
22 |
23 | # Create mockup file
24 | mockup_file = "mockup.txt"
25 | open(mockup_file, "wb").close()
26 |
27 | # Compute the MD5 has and compare the result with the expectation
28 | md5_mockup = SolarY.auxiliary.parse.comp_sha256(mockup_file)
29 | assert md5_mockup == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
30 |
31 | # Remove the mockup file from the system
32 | os.remove(mockup_file)
33 |
--------------------------------------------------------------------------------
/tests/test_auxiliary/test_reader.py:
--------------------------------------------------------------------------------
1 | """
2 | test_reader.py
3 |
4 | Testing suite for the reader functionality
5 |
6 | """
7 |
8 | # Import SolarY
9 | import SolarY
10 |
11 |
12 | def test_read_orbit():
13 | """
14 | Test the reader function read_orbit.
15 |
16 | Returns
17 | -------
18 | None.
19 |
20 | """
21 |
22 | # Get the test config file paths
23 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
24 |
25 | # Parse the orbit path
26 | test_orbit_path = SolarY.auxiliary.parse.get_test_file_path(
27 | "../" + test_paths_config["general_astrodyn"]["base_class_orbit"]
28 | )
29 |
30 | # Read and parse the orbit file and return a values and units dictionary
31 | test_orbit_values, test_orbit_units = SolarY.auxiliary.reader.read_orbit(
32 | test_orbit_path
33 | )
34 |
35 | # Check whether the instances are correct and the expectations
36 | assert isinstance(test_orbit_values, dict)
37 | assert isinstance(test_orbit_units, dict)
38 |
39 | assert test_orbit_values["peri"] == 1.133
40 | assert test_orbit_units["spatial"] == "AU"
41 |
42 |
43 | test_read_orbit()
44 |
--------------------------------------------------------------------------------
/tests/test_general/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_astrodyn
2 | from . import test_geometry
3 | from . import test_photometry
4 | from . import test_vec
5 |
--------------------------------------------------------------------------------
/tests/test_general/test_astrodyn.py:
--------------------------------------------------------------------------------
1 | """
2 | test_astrodyn.py
3 |
4 | Testing suite for SolarY/general/astrodyn.py
5 |
6 | """
7 | import math
8 |
9 | import pytest
10 |
11 | import SolarY
12 |
13 |
14 | @pytest.fixture(name="test_orbit_data")
15 | def fixture_test_orbit_data():
16 | """
17 | Fixture to load orbit example data.
18 |
19 | Returns
20 | -------
21 | test_orbit_values : dict
22 | Dictionary that contains the values.
23 | test_orbit_units : dict
24 | Dictionary that contains the units.
25 |
26 | """
27 |
28 | # Get the test config file paths
29 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
30 |
31 | test_orbit_path = SolarY.auxiliary.parse.get_test_file_path(
32 | "../" + test_paths_config["general_astrodyn"]["base_class_orbit"]
33 | )
34 |
35 | test_orbit_values, test_orbit_units = SolarY.auxiliary.reader.read_orbit(
36 | test_orbit_path
37 | )
38 |
39 | return test_orbit_values, test_orbit_units
40 |
41 |
42 | def test_tisserand():
43 | """
44 | Test function for the Tisserand computation function.
45 |
46 | Returns
47 | -------
48 | None.
49 |
50 | """
51 |
52 | # Compute Tisserand parameter (test case #1) and compare with expectation
53 | tisserand_parameter1 = SolarY.general.astrodyn.tisserand(
54 | sem_maj_axis_obj=5.0, inc=0.0, ecc=0.0
55 | )
56 | assert tisserand_parameter1 == 3.001200087241328
57 |
58 | # Compute Tisserand parameter (test case #2) and compare with expectation
59 | tisserand_parameter2 = SolarY.general.astrodyn.tisserand(
60 | sem_maj_axis_obj=4.0, inc=0.0, ecc=0.65
61 | )
62 | assert tisserand_parameter2 == 2.633422691976387
63 |
64 | # Compute Tisserand parameter (test case #3) and compare with expectation
65 | tisserand_parameter3 = SolarY.general.astrodyn.tisserand(
66 | sem_maj_axis_obj=4.0, inc=math.radians(30.0), ecc=0.65
67 | )
68 | assert tisserand_parameter3 == 2.454890564710888
69 |
70 | # Compute Tisserand parameter (test case #4) and compare with expectation
71 | tisserand_parameter4 = SolarY.general.astrodyn.tisserand(
72 | sem_maj_axis_obj=4.0, inc=math.radians(30.0), ecc=0.65, sem_maj_axis_planet=3.0
73 | )
74 | assert tisserand_parameter4 == 2.2698684153570663
75 |
76 |
77 | def test_kep_apoapsis():
78 | """
79 | Test function for the Apoapsis computation.
80 |
81 | Returns
82 | -------
83 | None.
84 |
85 | """
86 |
87 | # Compute the Apoapsis and perform assertion test (example #1)
88 | apoapsis1 = SolarY.general.astrodyn.kep_apoapsis(sem_maj_axis=5.0, ecc=0.3)
89 | assert apoapsis1 == 6.5
90 |
91 | # Compute the Apoapsis and perform assertion test (example #2)
92 | apoapsis2 = SolarY.general.astrodyn.kep_apoapsis(sem_maj_axis=10.0, ecc=0.0)
93 | assert apoapsis2 == 10
94 |
95 |
96 | def test_kep_periapsis():
97 | """
98 | Test function for the Periapsis computation.
99 |
100 | Returns
101 | -------
102 | None.
103 |
104 | """
105 |
106 | # Compute the Periapsis and perform assertion test (example #1)
107 | periapsis1 = SolarY.general.astrodyn.kep_periapsis(sem_maj_axis=5.0, ecc=0.3)
108 | assert periapsis1 == 3.5
109 |
110 | # Compute the Periapsis and perform assertion test (example #2)
111 | periapsis2 = SolarY.general.astrodyn.kep_periapsis(sem_maj_axis=10.0, ecc=0.0)
112 | assert periapsis2 == 10
113 |
114 |
115 | def test_mjd2jd():
116 | """
117 | Test function to verify Modified Julian Date to Julian Date converter function.
118 |
119 | Returns
120 | -------
121 | None.
122 |
123 | """
124 |
125 | # Compute the JD with a given MJD
126 | jd1 = SolarY.general.astrodyn.mjd2jd(m_juldate=56123.5)
127 | assert jd1 == 2456124
128 |
129 |
130 | def test_jd2mjd():
131 | """
132 | Test function to verify Julian Date to Modified Julian Date converter function.
133 |
134 | Returns
135 | -------
136 | None.
137 |
138 | """
139 |
140 | # Compute the MJD with a given JD
141 | mjd1 = SolarY.general.astrodyn.jd2mjd(juldate=2456000.5)
142 | assert mjd1 == 56000.0
143 |
144 |
145 | def test_sphere_of_influence():
146 | """
147 | Test function to check the Sphere Of Influence (SOI) computation function.
148 |
149 | Returns
150 | -------
151 | None.
152 |
153 | """
154 |
155 | # Read the constants config file and get the value for 1 AU, grav. constant, Earth's and Sun's
156 | # grav. constant (and convert both to mass)
157 | config = SolarY.auxiliary.config.get_constants()
158 | sem_maj_axis_earth = float(config["constants"]["one_au"])
159 | grav_const = float(config["constants"]["grav_const"])
160 | earth_mass = float(config["constants"]["gm_earth"]) / grav_const
161 | sun_mass = float(config["constants"]["gm_sun"]) / grav_const
162 |
163 | # Compute the SOI of planet Earth
164 | soi_res_earth = SolarY.general.astrodyn.sphere_of_influence(
165 | sem_maj_axis=sem_maj_axis_earth, minor_mass=earth_mass, major_mass=sun_mass
166 | )
167 |
168 | # Assertion test with the SOI's expectation
169 | assert pytest.approx(soi_res_earth, abs=1e4) == 925000.0
170 |
171 |
172 | def test_orbit(test_orbit_data):
173 | """
174 | Test function to check the orbit base class
175 |
176 | Parameters
177 | ----------
178 | test_orbit_data : tuple
179 | Tuple that contains 2 dictionaries; the orbit values and units.
180 |
181 | Returns
182 | -------
183 | None.
184 |
185 | """
186 |
187 | # Check if the fixture is loaded correctly
188 | assert isinstance(test_orbit_data, tuple)
189 |
190 | # Split the tuple into the 2 dictionaries
191 | test_orbit_values, test_orbit_units = test_orbit_data
192 |
193 | # Initiate the class
194 | test_orbit_class = SolarY.general.astrodyn.Orbit(
195 | orbit_values=test_orbit_values, orbit_units=test_orbit_units
196 | )
197 |
198 | # Check if the instances of the class correspond with the pre-defined settings
199 | assert test_orbit_class.peri == test_orbit_values["peri"]
200 | assert test_orbit_class.ecc == test_orbit_values["ecc"]
201 | assert test_orbit_class.incl == test_orbit_values["incl"]
202 | assert test_orbit_class.long_asc_node == test_orbit_values["long_asc_node"]
203 | assert test_orbit_class.arg_peri == test_orbit_values["arg_peri"]
204 |
205 | # Check the property: semi major axis
206 | assert test_orbit_class.semi_maj_axis == test_orbit_values["peri"] / (
207 | 1.0 - test_orbit_values["ecc"]
208 | )
209 |
210 | # Check the property: apoapsis
211 | assert (
212 | test_orbit_class.apo
213 | == (1.0 + test_orbit_values["ecc"]) * test_orbit_class.semi_maj_axis
214 | )
215 |
--------------------------------------------------------------------------------
/tests/test_general/test_geometry.py:
--------------------------------------------------------------------------------
1 | """
2 | test_geometry.py
3 |
4 | Testing suite for SolarY/general/geometry.py
5 |
6 | """
7 |
8 | # Import standard libraries
9 | import math
10 |
11 | import SolarY
12 |
13 |
14 | def test_circle_area():
15 | """
16 | Testing the area computation of a perfect circle.
17 |
18 | Returns
19 | -------
20 | None.
21 |
22 | """
23 |
24 | # Compute the area of a unit circle
25 | circle_area_res1 = SolarY.general.geometry.circle_area(radius=1.0)
26 | assert circle_area_res1 == math.pi
27 |
28 | # Second example
29 | circle_area_res1 = SolarY.general.geometry.circle_area(radius=2.0)
30 | assert circle_area_res1 == math.pi * 4.0
31 |
32 |
33 | def test_fwhm2std():
34 | """
35 | Testing the FWHM to standard deviation computation
36 |
37 | Returns
38 | -------
39 | None.
40 |
41 | """
42 |
43 | # Compute the standard deviation corresponding FWHM and vice versa
44 | sigma1_exp = 5.0
45 | fwhm1 = 2.0 * sigma1_exp * math.sqrt(2.0 * math.log(2))
46 |
47 | sigma1_res = SolarY.general.geometry.fwhm2std(fwhm1)
48 | assert sigma1_res == sigma1_exp
49 |
--------------------------------------------------------------------------------
/tests/test_general/test_photometry.py:
--------------------------------------------------------------------------------
1 | """
2 | test_photometry.py
3 |
4 | Testing suite for SolarY/general/photometry.py
5 |
6 | """
7 |
8 | # Import standard libraries
9 | import math
10 |
11 | # Import installed libraries
12 | import pytest
13 |
14 | import SolarY
15 |
16 |
17 | def test_appmag2irr():
18 | """
19 | Testing the function appmag2irr that converts the apparent magnitude to an irradiance in W/m^2
20 |
21 | Returns
22 | -------
23 | None.
24 |
25 | """
26 |
27 | # Read the constants file and get the bolometric zero value
28 | config = SolarY.auxiliary.config.get_constants()
29 | appmag_irr_i0 = float(config["photometry"]["appmag_irr_i0"])
30 |
31 | # The apparent magnitude of 0 mag must correspond to the bolometric 0 value
32 | irradiance1 = SolarY.general.photometry.appmag2irr(app_mag=0)
33 | assert pytest.approx(irradiance1) == appmag_irr_i0
34 |
35 | # -5 mag must be 100 brighter than the bolometric zero value
36 | irradiance2 = SolarY.general.photometry.appmag2irr(app_mag=-5)
37 | assert pytest.approx(irradiance2) == appmag_irr_i0 * 100.0
38 |
39 | # Another example
40 | irradiance3 = SolarY.general.photometry.appmag2irr(app_mag=1)
41 | assert pytest.approx(irradiance3) == 1.0024422165005002e-08
42 |
43 |
44 | def test_intmag2surmag():
45 | """
46 | Testing the function intmag2surmag that converts the integrated magnitude and over all area of
47 | the object to a surface brightness given in mag/arcsec^2.
48 |
49 | Returns
50 | -------
51 | None.
52 |
53 | """
54 |
55 | # Set a simple example, since the equation is quite trivial
56 | surf_bright = SolarY.general.photometry.intmag2surmag(intmag=10.0, area=10.0)
57 | assert surf_bright == 12.5
58 |
59 |
60 | def test_surmag2intmag():
61 | """
62 | Testing the function surmag2intmag that converts the surface brightness and area to a
63 | corresponding integrated magnitude.
64 |
65 | Returns
66 | -------
67 | None.
68 |
69 | """
70 |
71 | # Set the same example as shown for the function intmag2surmag
72 | intmag = SolarY.general.photometry.surmag2intmag(surmag=12.5, area=10.0)
73 | assert intmag == 10.0
74 |
75 |
76 | def test_phase_func():
77 | """
78 | This function tests the phase function that is needed for the apparent magnitude computation of
79 | minor bodies
80 |
81 | Returns
82 | -------
83 | None.
84 |
85 | """
86 |
87 | # Example 1
88 | phi1_res1 = SolarY.general.photometry.phase_func(index=1, phase_angle=0.0)
89 | assert phi1_res1 == 1.0
90 |
91 | # Example 2
92 | phi2_res1 = SolarY.general.photometry.phase_func(index=1, phase_angle=0.0)
93 | assert phi2_res1 == 1.0
94 |
95 | # Example 3
96 | phi1_res2 = SolarY.general.photometry.phase_func(index=1, phase_angle=math.pi / 2.0)
97 | assert phi1_res2 == 0.03579310506765532
98 |
99 | # Example 4
100 | phi2_res2 = SolarY.general.photometry.phase_func(index=2, phase_angle=math.pi / 2.0)
101 | assert phi2_res2 == 0.15412366181513143
102 |
103 |
104 | def test_reduc_mag():
105 | """
106 | Testing the function that computed the reduced magnitude.
107 |
108 | Returns
109 | -------
110 | None.
111 |
112 | """
113 |
114 | # An absolute magnitude of 0 and a phase angle of 0 degrees must correspond to a reduced
115 | # magnitude of 0
116 | red_mag1 = SolarY.general.photometry.reduc_mag(
117 | abs_mag=0, slope_g=0.15, phase_angle=0.0
118 | )
119 | assert red_mag1 == 0.0
120 |
121 | # Second artifically set and computed example
122 | red_mag2 = SolarY.general.photometry.reduc_mag(
123 | abs_mag=0, slope_g=0.15, phase_angle=math.pi / 2.0
124 | )
125 | assert red_mag2 == 3.178249562605391
126 |
127 |
128 | def test_hg_app_mag():
129 | """
130 | Testing the computation of the apparent magnitude (H-G system).
131 |
132 | Returns
133 | -------
134 | None.
135 |
136 | """
137 |
138 | # Set sample vectors (example: first vector is an asteroid at 2 AU, second vector is planet
139 | # Earth at 1 AU)
140 | vec_obj1 = [2.0, 0.0, 0.0]
141 | vec_obs1 = [1.0, 0.0, 0.0]
142 |
143 | # Compute the vector object -> observer
144 | vec_obj2obs1 = SolarY.general.vec.substract(vector1=vec_obs1, vector2=vec_obj1)
145 |
146 | # Compute the vector object -> illumination source (by inversing the object vector to
147 | # [-2.0, 0.0, 0.0])
148 | vec_obj2ill1 = SolarY.general.vec.inverse(vector=vec_obj1)
149 |
150 | # Compute the apparent magnitude for an object with an absolute magnitude of 0.0 mag and a
151 | # slope parameter of 0.15 (default value for asteroids).
152 | app_mag1 = SolarY.general.photometry.hg_app_mag(
153 | abs_mag=0.0, slope_g=0.15, vec_obj2obs=vec_obj2obs1, vec_obj2ill=vec_obj2ill1
154 | )
155 | assert app_mag1 == 1.505149978319906
156 |
157 | # Second example with simplified values for (1) Ceres (at opposition w.r.t. Earth)
158 | vec_obj2 = [3.0, 0.0, 0.0]
159 | vec_obs2 = [1.0, 0.0, 0.0]
160 |
161 | vec_obj2obs2 = SolarY.general.vec.substract(vector1=vec_obs2, vector2=vec_obj2)
162 | vec_obj2ill2 = SolarY.general.vec.inverse(vector=vec_obj2)
163 |
164 | app_mag2 = SolarY.general.photometry.hg_app_mag(
165 | abs_mag=3.4, slope_g=0.12, vec_obj2obs=vec_obj2obs2, vec_obj2ill=vec_obj2ill2
166 | )
167 |
168 | # Ceres has a brightness of around 7 mag at opposition
169 | assert app_mag2 == 7.290756251918218
170 |
171 | # Third example with the same values but a larger phase angle
172 | vec_obj3 = [0.0, 3.0, 0.0]
173 | vec_obs3 = [1.0, 0.0, 0.0]
174 |
175 | vec_obj2obs3 = SolarY.general.vec.substract(vector1=vec_obs3, vector2=vec_obj3)
176 | vec_obj2ill3 = SolarY.general.vec.inverse(vector=vec_obj3)
177 |
178 | app_mag3 = SolarY.general.photometry.hg_app_mag(
179 | abs_mag=3.4, slope_g=0.12, vec_obj2obs=vec_obj2obs3, vec_obj2ill=vec_obj2ill3
180 | )
181 |
182 | # A larger phase angle should lead to a smaller brightness (larger apparent magnitude);
183 | # compared to the opposition result
184 | assert app_mag3 > app_mag2
185 |
--------------------------------------------------------------------------------
/tests/test_general/test_vec.py:
--------------------------------------------------------------------------------
1 | """
2 | test_vec.py
3 |
4 | Testing suite for SolarY/general/vec.py
5 |
6 | """
7 |
8 | # Import standard libraries
9 | import math
10 |
11 | # Import installed libraries
12 | import pytest
13 |
14 | import SolarY
15 |
16 |
17 | def test_norm():
18 | """
19 | Testing the vector norm computation.
20 |
21 | Returns
22 | -------
23 | None.
24 |
25 | """
26 |
27 | # Compute the length of a unit vector
28 | vec_norm_res1 = SolarY.general.vec.norm(vector=[1.0, 0.0])
29 | assert vec_norm_res1 == 1.0
30 |
31 | # Compute the length of a 45 degrees vector
32 | vec_norm_res2 = SolarY.general.vec.norm(vector=[1.0, 1.0])
33 | assert vec_norm_res2 == math.sqrt(2)
34 |
35 | # Example #3
36 | vec_norm_res3 = SolarY.general.vec.norm(vector=[5.0, 4.0])
37 | assert vec_norm_res3 == math.sqrt(41)
38 |
39 | # Example #4
40 | vec_norm_res4 = SolarY.general.vec.norm(vector=[1.0, 1.0, 1.0])
41 | assert vec_norm_res4 == math.sqrt(3)
42 |
43 | # Example #5
44 | vec_norm_res5 = SolarY.general.vec.norm(vector=[2.0, 4.0, -5.0, 6.0])
45 | assert vec_norm_res5 == 9.0
46 |
47 |
48 | def test_unify():
49 | """
50 | Testing the function that normalises a vector.
51 |
52 | Returns
53 | -------
54 | None.
55 |
56 | """
57 |
58 | # A unified unit vector must correspond to the input
59 | unit_vec1 = SolarY.general.vec.unify(vector=[1.0, 0.0, 0.0])
60 | assert unit_vec1 == [1.0, 0.0, 0.0]
61 |
62 | # Example #2
63 | unit_vec2 = SolarY.general.vec.unify(vector=[5.0, 0.0, 0.0])
64 | assert unit_vec2 == [1.0, 0.0, 0.0]
65 |
66 | # Example #3
67 | unit_vec3 = SolarY.general.vec.unify(vector=[5.0, 5.0, 5.0])
68 | assert pytest.approx(unit_vec3) == [
69 | 1.0 / math.sqrt(3),
70 | 1.0 / math.sqrt(3),
71 | 1.0 / math.sqrt(3),
72 | ]
73 |
74 |
75 | def test_dot_prod():
76 | """
77 | Testing suite for the computation of the dot product.
78 |
79 | Returns
80 | -------
81 | None.
82 |
83 | """
84 |
85 | # Example #1
86 | dot_res1 = SolarY.general.vec.dot_prod(
87 | vector1=[1.0, 2.0, 3.0], vector2=[-2.0, 5.0, 8.0]
88 | )
89 | assert dot_res1 == 32.0
90 |
91 | # Example #2
92 | dot_res2 = SolarY.general.vec.dot_prod(vector1=[-10.0, 20.0], vector2=[-1.0, 0.0])
93 | assert dot_res2 == 10.0
94 |
95 | # Example #3
96 | dot_res3 = SolarY.general.vec.dot_prod(vector1=[23.0, 10.0], vector2=[2.0, 0.01])
97 | assert dot_res3 == 46.1
98 |
99 |
100 | def test_phase_angle():
101 | """
102 | Testing the computation of the phase angle between two vectors.
103 |
104 | Returns
105 | -------
106 | None.
107 |
108 | """
109 |
110 | # Example #1: Phase angle of 90 degrees
111 | angle_res1 = SolarY.general.vec.phase_angle(vector1=[1.0, 0.0], vector2=[0.0, 1.0])
112 | assert angle_res1 == math.pi / 2.0
113 |
114 | # Example #2: Phase angle of 180 degrees
115 | angle_res2 = SolarY.general.vec.phase_angle(vector1=[1.0, 0.0], vector2=[-1.0, 0.0])
116 | assert angle_res2 == math.pi
117 |
118 | # Example #3: Phase angle of 0 degrees
119 | angle_res3 = SolarY.general.vec.phase_angle(vector1=[1.0, 0.0], vector2=[1.0, 0.0])
120 | assert angle_res3 == 0.0
121 |
122 |
123 | def test_substract():
124 | """
125 | Testing the substraction of two vectors.
126 |
127 | Returns
128 | -------
129 | None.
130 |
131 | """
132 |
133 | # Example #1
134 | vec_diff1 = SolarY.general.vec.substract(vector1=[4.0, 7.0], vector2=[5.0, 1.0])
135 | assert vec_diff1 == [-1.0, 6.0]
136 |
137 | # Example #2
138 | vec_diff2 = SolarY.general.vec.substract(vector1=[-4.0, -4.0], vector2=[-5.0, 9.0])
139 | assert vec_diff2 == [1.0, -13.0]
140 |
141 |
142 | def test_inverse():
143 | """
144 | Testing the vector inversion.
145 |
146 | Returns
147 | -------
148 | None.
149 |
150 | """
151 |
152 | # Example #1
153 | inverse_vec1 = SolarY.general.vec.inverse(vector=[1.0, 5.0])
154 | assert inverse_vec1 == [-1.0, -5.0]
155 |
156 | # Example #2
157 | inverse_vec2 = SolarY.general.vec.inverse(vector=[-5.1, -100.0, 0.0])
158 | assert inverse_vec2 == [5.1, 100.0, 0.0]
159 |
--------------------------------------------------------------------------------
/tests/test_instruments/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_camera
2 | from . import test_optics
3 | from . import test_telescope
4 |
--------------------------------------------------------------------------------
/tests/test_instruments/test_camera.py:
--------------------------------------------------------------------------------
1 | """
2 | test_camera.py
3 |
4 | Testing suite for SolarY/instruments/camera.py
5 |
6 | """
7 |
8 | # Import installed libraries
9 | import pytest
10 |
11 | import SolarY
12 |
13 |
14 | # Define a test fixture that is being used in all tests. The fixture loads configuration files.
15 | @pytest.fixture(name="ccd_test_config")
16 | def fixture_ccd_test_config():
17 | """
18 | Fixture to load the test configuration files.
19 |
20 | Returns
21 | -------
22 | test_ccd_dict : dict
23 | CCD test configuration.
24 |
25 | """
26 |
27 | # Get the test config file paths
28 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
29 |
30 | # Get the path to the CCD config file
31 | test_ccd_path = SolarY.auxiliary.parse.get_test_file_path(
32 | "../" + test_paths_config["instruments_camera_ccd"]["properties"]
33 | )
34 |
35 | # Read and parse the CCD config file and return a dictionary with the properties
36 | test_ccd_dict = SolarY.instruments.camera.CCD.load_from_json_file(test_ccd_path)
37 |
38 | return test_ccd_dict
39 |
40 |
41 | def test_read_ccd_config(ccd_test_config):
42 | """
43 | Testing if the config file reading was successful.
44 |
45 | Parameters
46 | ----------
47 | ccd_test_config : SolarY.instruments.camera.CCD
48 | CCD config object.
49 |
50 | Returns
51 | -------
52 | None.
53 |
54 | """
55 |
56 | # Check if the fixture load is a dictionary
57 | assert isinstance(ccd_test_config, SolarY.instruments.camera.CCD)
58 |
59 | # Check the pixels
60 | assert ccd_test_config.pixels == [4096, 4112]
61 |
62 |
63 | def test_ccd(ccd_test_config):
64 | """
65 | Testing the CCD Class.
66 |
67 | Parameters
68 | ----------
69 | ccd_test_config : SolarY.instruments.camera.CCD
70 | CCD config object.
71 |
72 | Returns
73 | -------
74 | None.
75 |
76 | """
77 |
78 | # Initiate the CCD class
79 | # test_ccd_class = SolarY.instruments.camera.CCD(**ccd_test_config)
80 |
81 | # Check the config depending attributes
82 | assert ccd_test_config.pixels == [4096, 4112]
83 | assert ccd_test_config.pixel_size == 15.0
84 | assert ccd_test_config.dark_noise == 2.0
85 | assert ccd_test_config.readout_noise == 1.0
86 | assert ccd_test_config.full_well == 300000.0
87 | assert ccd_test_config.quantum_eff == 0.5
88 |
89 | # Check the first entry of the chip size. Multiply the number of pixels (x dimension) times the
90 | # pixel size in micro meters. Finally divide by 1000 to get a result in mm.
91 | assert (
92 | ccd_test_config.chip_size[0]
93 | == ccd_test_config.pixels[0] * ccd_test_config.pixel_size / 1000.0
94 | )
95 |
96 | # Check the size of a single pixel in m^2. The pixel size squared leads to micro meter squared.
97 | # Divide the results by 10^-12.
98 | assert ccd_test_config.pixel_size_sq_m == (
99 | ccd_test_config.pixel_size ** 2.0
100 | ) * 10.0 ** (-12.0)
101 |
--------------------------------------------------------------------------------
/tests/test_instruments/test_optics.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=no-member
2 | """
3 | test_optics.py
4 |
5 | Testing suite for SolarY/instruments/optics.py
6 |
7 | """
8 |
9 | # Import installed libraries
10 | import pytest
11 |
12 | import SolarY
13 |
14 |
15 | # Define a test fixture that is being used in all tests. The fixture loads configuration files.
16 | @pytest.fixture(name="reflector_test_optics")
17 | def fixture_reflector_test_optics():
18 | """
19 | Fixture to load the test configuration files.
20 |
21 | Returns
22 | -------
23 | test_reflector_obj : SolarY.instruments.optics.Reflector
24 | Reflector object.
25 |
26 | """
27 |
28 | # Get the test config file paths
29 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
30 |
31 | # Get the path to the relfector config file
32 | test_reflector_path = SolarY.auxiliary.parse.get_test_file_path(
33 | "../" + test_paths_config["instruments_optics_reflector"]["properties"]
34 | )
35 |
36 | # Read and parse the reflector config file and return a dictionary with the properties
37 | test_reflector_obj = SolarY.instruments.optics.Reflector.load_from_json_file(
38 | test_reflector_path
39 | )
40 |
41 | return test_reflector_obj
42 |
43 |
44 | def test_read_optical_config(reflector_test_optics):
45 | """
46 | Testing if the config file reading was successful.
47 |
48 | Parameters
49 | ----------
50 | reflector_test_optics : SolarY.instruments.optics.Reflector
51 | Reflector object.
52 |
53 | Returns
54 | -------
55 | None.
56 |
57 | """
58 |
59 | # Check if the fixture load is a dictionary
60 | assert isinstance(reflector_test_optics, SolarY.instruments.optics.Reflector)
61 |
62 | # Check the main mirror entry
63 | assert reflector_test_optics.main_mirror_dia == 1.0
64 |
65 |
66 | def test_reflector(reflector_test_optics):
67 | """
68 | Testing the Reflector Class.
69 |
70 | Parameters
71 | ----------
72 | reflector_test_optics : SolarY.instruments.optics.Reflector
73 | Reflector object.
74 |
75 | Returns
76 | -------
77 | None.
78 |
79 | """
80 |
81 | # Initiate the Reflector class
82 | # test_reflector_class = SolarY.instruments.optics.Reflector(reflector_test_optics)
83 |
84 | # Check if the instances of the class correspond to the config file
85 | assert reflector_test_optics.main_mirror_dia == 1.0
86 | assert reflector_test_optics.sec_mirror_dia == 0.2
87 | assert reflector_test_optics.optical_throughput == 0.6
88 | assert reflector_test_optics.focal_length == 10.0
89 |
90 | # Check the main mirror area that depends on the diameter of the main mirror
91 | assert (
92 | reflector_test_optics.main_mirror_area
93 | == SolarY.general.geometry.circle_area(
94 | radius=reflector_test_optics.main_mirror_dia / 2.0
95 | )
96 | )
97 |
98 | # Check the secondary mirror area that depends on the diameter of the secondary mirror
99 | assert reflector_test_optics.sec_mirror_area == SolarY.general.geometry.circle_area(
100 | radius=reflector_test_optics.sec_mirror_dia / 2.0
101 | )
102 |
103 | # Check now the total collect area
104 | assert (
105 | reflector_test_optics.collect_area
106 | == reflector_test_optics.main_mirror_area
107 | - reflector_test_optics.sec_mirror_area
108 | )
109 |
--------------------------------------------------------------------------------
/tests/test_instruments/test_telescope.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=W0212
2 | """
3 | test_telescope.py
4 |
5 | Testing suite for SolarY/instruments/telescope.py
6 |
7 | """
8 | import math
9 |
10 | import pytest
11 |
12 | import SolarY
13 |
14 |
15 | @pytest.fixture(name="telescope_test_obj")
16 | def fixture_telescope_test_obj():
17 | """
18 | Fixture to load the test configuration files.
19 |
20 |
21 | Returns
22 | -------
23 | test_reflector_ccd : SolarY.instruments.telescope.ReflectorCCD
24 | Reflector test object.
25 |
26 | """
27 |
28 | # Get the test config paths
29 | test_paths_config = SolarY.auxiliary.config.get_paths(test=True)
30 |
31 | # Load and parse the reflector config
32 | test_reflector_path = SolarY.auxiliary.parse.get_test_file_path(
33 | "../" + test_paths_config["instruments_optics_reflector"]["properties"]
34 | )
35 |
36 | test_reflector = SolarY.instruments.optics.Reflector.load_from_json_file(
37 | test_reflector_path
38 | )
39 |
40 | # Load and parse the CCD properties
41 | test_ccd_path = SolarY.auxiliary.parse.get_test_file_path(
42 | "../" + test_paths_config["instruments_camera_ccd"]["properties"]
43 | )
44 |
45 | test_ccd = SolarY.instruments.camera.CCD.load_from_json_file(test_ccd_path)
46 |
47 | test_reflector_ccd = SolarY.instruments.telescope.ReflectorCCD.load_from_json_files(
48 | optics_path=test_reflector_path,
49 | ccd_path=test_ccd_path,
50 | )
51 | return test_reflector_ccd
52 |
53 |
54 | def test_comp_fov():
55 | """
56 | Test the Field-Of-View computation function
57 |
58 | Returns
59 | -------
60 | None.
61 |
62 | """
63 |
64 | # Test sample with a sensor dimension / size of 25.4 mm and a telescope focial length of 1000
65 | # mm
66 | fov1 = SolarY.instruments.telescope.comp_fov(sensor_dim=25.4, focal_length=1000.0)
67 | assert pytest.approx(fov1 / 60.0, abs=0.1) == 87.3
68 |
69 |
70 | def test_reflectorccd(telescope_test_obj):
71 | """
72 | Testing the telescope class. To compare the rather complex and error-prone computation, a PERL
73 | based script from [1] has been used for rough computation comparisons.
74 |
75 | Parameters
76 | ----------
77 | test_reflector_ccd : SolarY.instruments.telescope.ReflectorCCD
78 | Reflector test object.
79 |
80 | Returns
81 | -------
82 | None.
83 |
84 | References
85 | ----------
86 | [1] http://spiff.rit.edu/richmond/signal.shtml 04.Jan.2021
87 |
88 | """
89 |
90 | # Set some settings for later use
91 | photometric_aperture = 10.0
92 | half_flux_diameter = 10.0
93 | expos_time = 60.0
94 |
95 | # Set some observational parameters
96 | object_brightness = 19.0
97 | sky_brightness = 19.0
98 |
99 | # Check if the property tuple is correctly formatted
100 | assert isinstance(telescope_test_obj, SolarY.instruments.telescope.ReflectorCCD)
101 |
102 | # Check attributes
103 | assert telescope_test_obj.ccd.pixels == telescope_test_obj.ccd.pixels
104 | assert (
105 | telescope_test_obj.optics.main_mirror_dia
106 | == telescope_test_obj.optics.main_mirror_dia
107 | )
108 |
109 | # Test if constants config has been loaded
110 | config = SolarY.auxiliary.config.get_constants()
111 | assert telescope_test_obj._photon_flux_v == float(
112 | config["photometry"]["photon_flux_V"]
113 | )
114 |
115 | # Test now the telescope specific properties, FOV
116 | assert pytest.approx(telescope_test_obj.fov[0], abs=0.1) == 1266.8
117 | assert pytest.approx(telescope_test_obj.fov[1], abs=0.1) == 1271.8
118 |
119 | # Check iFOV
120 | assert (
121 | telescope_test_obj.ifov[0]
122 | == telescope_test_obj.fov[0] / telescope_test_obj.ccd.pixels[0]
123 | )
124 |
125 | assert (
126 | telescope_test_obj.ifov[1]
127 | == telescope_test_obj.fov[1] / telescope_test_obj.ccd.pixels[1]
128 | )
129 |
130 | # Set aperture in arcsec
131 | telescope_test_obj.aperture = photometric_aperture
132 | assert telescope_test_obj.aperture == photometric_aperture
133 |
134 | # Set the half flux diameter in arcsec
135 | telescope_test_obj.hfdia = half_flux_diameter
136 | assert telescope_test_obj.hfdia == half_flux_diameter
137 |
138 | # Check how many pixels are aperture
139 | assert telescope_test_obj.pixels_in_aperture == int(
140 | round(
141 | SolarY.general.geometry.circle_area(0.5 * telescope_test_obj.aperture)
142 | / math.prod(telescope_test_obj.ifov),
143 | 0,
144 | )
145 | )
146 |
147 | # Set exposure time in seconds
148 | telescope_test_obj.exposure_time = expos_time
149 | assert telescope_test_obj.exposure_time == expos_time
150 |
151 | # Compute the light fraction in aperture
152 | assert (
153 | telescope_test_obj._ratio_light_aperture
154 | == math.erf(
155 | (telescope_test_obj.aperture)
156 | / (
157 | SolarY.general.geometry.fwhm2std(telescope_test_obj.hfdia)
158 | * math.sqrt(2)
159 | )
160 | )
161 | ** 2.0
162 | )
163 |
164 | # Compute raw object electrons (expectation)
165 | exp_e_signal = round(
166 | 10.0 ** (-0.4 * object_brightness)
167 | * float(config["photometry"]["photon_flux_V"])
168 | * expos_time
169 | * telescope_test_obj.optics.collect_area
170 | * telescope_test_obj.ccd.quantum_eff
171 | * telescope_test_obj.optics.optical_throughput
172 | * telescope_test_obj._ratio_light_aperture,
173 | 0,
174 | )
175 |
176 | # Compute the electron signal and compare the results
177 | object_electrons_apert = telescope_test_obj.object_esignal(mag=object_brightness)
178 | assert object_electrons_apert == exp_e_signal
179 |
180 | # Compute sky background signal (expectation)
181 | # Convert surface brightness to integrated brightness over entire FOV
182 | total_sky_mag = SolarY.general.photometry.surmag2intmag(
183 | surmag=sky_brightness, area=math.prod(telescope_test_obj.fov)
184 | )
185 | exp_sky_esignal = round(
186 | 10.0 ** (-0.4 * total_sky_mag)
187 | * float(config["photometry"]["photon_flux_V"])
188 | * expos_time
189 | * telescope_test_obj.optics.collect_area
190 | * telescope_test_obj.ccd.quantum_eff
191 | * telescope_test_obj.optics.optical_throughput
192 | * (
193 | telescope_test_obj.pixels_in_aperture
194 | / math.prod(telescope_test_obj.ccd.pixels)
195 | ),
196 | 0,
197 | )
198 |
199 | # Compare computation with expectation
200 | sky_electrons_apert = telescope_test_obj.sky_esignal(mag_arcsec_sq=sky_brightness)
201 | assert sky_electrons_apert == exp_sky_esignal
202 |
203 | # Test the dark current
204 | assert telescope_test_obj.dark_esignal_aperture == round(
205 | float(telescope_test_obj.ccd.dark_noise)
206 | * expos_time
207 | * telescope_test_obj.pixels_in_aperture,
208 | 0,
209 | )
210 |
211 | # Now test the SNR
212 | assert (
213 | pytest.approx(
214 | telescope_test_obj.object_snr(
215 | obj_mag=object_brightness, sky_mag_arcsec_sq=sky_brightness
216 | ),
217 | abs=0.1,
218 | )
219 | == 5.0
220 | )
221 |
--------------------------------------------------------------------------------
/tests/test_neo/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_astrodyn
2 | from . import test_data
3 |
--------------------------------------------------------------------------------
/tests/test_neo/test_astrodyn.py:
--------------------------------------------------------------------------------
1 | """Testing suite for SolarY/neo/astrodyn.py"""
2 |
3 | # Import SolarY
4 | import SolarY
5 |
6 |
7 | def test_neo_class():
8 | """
9 | Testing the neo classification function
10 |
11 | Returns
12 | -------
13 | None.
14 |
15 | """
16 |
17 | # Based on values from
18 | # 1221 Amor
19 | # 1862 Apollo
20 | # 2062 Aten
21 | # 163693 Atira
22 | neo_sample = {
23 | "Amor": {"sem_maj_axis_au": 1.9198, "peri_helio_au": 1.0856, "ap_helio_au": 2.7540},
24 | "Apollo": {"sem_maj_axis_au": 1.4702, "peri_helio_au": 0.64699, "ap_helio_au": 2.2935},
25 | "Aten": {"sem_maj_axis_au": 0.967, "peri_helio_au": 0.79, "ap_helio_au": 1.143},
26 | "Atira": {"sem_maj_axis_au": 0.7411, "peri_helio_au": 0.5024, "ap_helio_au": 0.9798},
27 | }
28 |
29 | # Iterate trough all NEO class expectations
30 | for _neo_class_exp in neo_sample:
31 |
32 | # Determine the NEO class
33 | neo_class_res = SolarY.neo.astrodyn.neo_class(
34 | sem_maj_axis_au=neo_sample[_neo_class_exp]["sem_maj_axis_au"],
35 | peri_helio_au=neo_sample[_neo_class_exp]["peri_helio_au"],
36 | ap_helio_au=neo_sample[_neo_class_exp]["ap_helio_au"],
37 | )
38 |
39 | # Check the classification with the expectation
40 | assert neo_class_res == _neo_class_exp
41 |
--------------------------------------------------------------------------------
/tests/test_neo/test_data.py:
--------------------------------------------------------------------------------
1 | """
2 | test_data.py
3 |
4 | Testing suite for SolarY/neo/data.py
5 |
6 | """
7 | import sqlite3
8 |
9 | import pytest
10 |
11 | import SolarY
12 |
13 |
14 | def test__get_neodys_neo_nr():
15 | """
16 | Testing the hidden function that gets the current number of known NEOs from the NEODyS webpage.
17 |
18 | Returns
19 | -------
20 | None.
21 |
22 | """
23 |
24 | # Call the function to get the number of currently known NEOs.
25 | neo_nr = SolarY.neo.data._get_neodys_neo_nr()
26 |
27 | # Check of the result is an integer
28 | assert isinstance(neo_nr, int)
29 |
30 | # Since the number of NEOs can change daily this assertion test check only if the number is
31 | # larger than 0
32 | assert neo_nr >= 0
33 |
34 |
35 | def test_download():
36 | """
37 | Testing the NEODyS download function
38 |
39 | Returns
40 | -------
41 | None.
42 |
43 | """
44 |
45 | # Execute the download function and check if the download status was "OK"
46 | dl_status, _ = SolarY.neo.data.download()
47 | assert dl_status == "OK"
48 |
49 | # Execute the donwload a second time get also the row expectation (internally it compares the
50 | # results with the _get_neodys_neo_nr function)
51 | dl_status, row_exp = SolarY.neo.data.download(row_exp=True)
52 | assert dl_status == "OK"
53 | assert isinstance(row_exp, int)
54 | assert row_exp >= 0
55 |
56 |
57 | def test_read_neodys():
58 | """
59 | Test the reading functionality of the downloaded NEODyS data
60 |
61 | Returns
62 | -------
63 | None.
64 |
65 | """
66 |
67 | # Read the data
68 | neo_dict_data = SolarY.neo.data.read_neodys()
69 |
70 | # The first entry must be (433) Erors; with its semi-major axis givne in AU and the
71 | # eccentricity
72 | assert neo_dict_data[0]["Name"] == "433"
73 | assert pytest.approx(neo_dict_data[0]["SemMajAxis_AU"], abs=1e-2) == 1.46
74 | assert pytest.approx(neo_dict_data[0]["Ecc_"], abs=1e-2) == 0.22
75 |
76 |
77 | def test_NEOdysDatabase():
78 | """
79 | Test the NEODyS database.
80 |
81 | Returns
82 | -------
83 | None.
84 |
85 | """
86 |
87 | # Create the database and check if the connection is established and the cursor is set
88 | neo_sqlite = SolarY.neo.data.NEOdysDatabase(new=True)
89 |
90 | assert isinstance(neo_sqlite.con, sqlite3.Connection)
91 | assert isinstance(neo_sqlite.cur, sqlite3.Cursor)
92 |
93 | # Create the table and content
94 | neo_sqlite.create()
95 |
96 | # Get now the data of (433) Eros
97 | query_res_cur = neo_sqlite.cur.execute(
98 | "SELECT Name, SemMajAxis_AU, ECC_ " 'FROM main WHERE Name = "433"'
99 | )
100 | query_res = query_res_cur.fetchone()
101 |
102 | # Perform assertion tests on Eros' data
103 | assert query_res[0] == "433"
104 | assert pytest.approx(query_res[1], abs=1e-2) == 1.46
105 | assert pytest.approx(query_res[2], abs=1e-2) == 0.22
106 |
107 | # Compute the derived orbital elements and the the aphelion and perihelion data of Eros
108 | neo_sqlite.create_deriv_orb()
109 | query_res_cur = neo_sqlite.cur.execute(
110 | "SELECT Name, Aphel_AU, Perihel_AU " 'FROM main WHERE Name = "433"'
111 | )
112 | query_res = query_res_cur.fetchone()
113 |
114 | # Perform assertion tests on Eros' derived results
115 | assert query_res[0] == "433"
116 | assert pytest.approx(query_res[1], abs=1e-3) == 1.783
117 | assert pytest.approx(query_res[2], abs=1e-3) == 1.133
118 |
119 | # Compute the NEO class and check the result for Eros
120 | neo_sqlite.create_neo_class()
121 | query_res_cur = neo_sqlite.cur.execute(
122 | "SELECT Name, NEOClass " 'FROM main WHERE Name = "433"'
123 | )
124 | query_res = query_res_cur.fetchone()
125 |
126 | # Perform assertion tests on Eros' derived results
127 | assert query_res[0] == "433"
128 | assert query_res[1] == "Amor"
129 |
130 | # Now the test check if the update functionality works. For this purpose, the first row from the
131 | # database is deleted; the update function is executed and then the number of rows is compared
132 | # with the expectation.
133 | # First: get the current number of rows from the database
134 | query_res_cur = neo_sqlite.cur.execute("SELECT COUNT(*) FROM main")
135 | original_count = query_res_cur.fetchone()[0]
136 |
137 | # Delete Eros and get the number of rows
138 | neo_sqlite.cur.execute('DELETE FROM main WHERE Name="433"')
139 | neo_sqlite.con.commit()
140 | query_res_cur = neo_sqlite.cur.execute("SELECT COUNT(*) FROM main")
141 | manip_count = query_res_cur.fetchone()[0]
142 |
143 | # The modified database should have 1 entry less than before
144 | assert manip_count == original_count - 1
145 |
146 | # Update the database and get the number of counts
147 | neo_sqlite.update()
148 | query_res_cur = neo_sqlite.cur.execute("SELECT COUNT(*) FROM main")
149 | update_count = query_res_cur.fetchone()[0]
150 |
151 | # Now the updated database must have the same number of rows as before
152 | assert update_count == original_count
153 |
154 | # Close the database
155 | neo_sqlite.close()
156 |
157 |
158 | def test_download_granvik2018():
159 | """
160 | Testing the download of the Granvik et al. (2018) NEO data.
161 |
162 | Returns
163 | -------
164 | None.
165 |
166 | """
167 |
168 | # Download the data and compare the MD5 hash of the downloaded file with the expectation
169 | sha256_hash = SolarY.neo.data.download_granvik2018()
170 | assert sha256_hash == "759390f9fec799290eea14ef322dbfbdd05c00c2770e49765a43199f2cf9cc6e"
171 |
172 |
173 | def test_read_granvik2018():
174 | """
175 | Test the reader function of the Granvik et al. (2018) data.
176 |
177 | Returns
178 | -------
179 | None.
180 |
181 | """
182 |
183 | # Read the data and perform some assertions (expectations for the very first entry that has
184 | # been inspected before)
185 | neo_dict_data = SolarY.neo.data.read_granvik2018()
186 | assert pytest.approx(neo_dict_data[0]["SemMajAxis_AU"]) == 2.57498121
187 | assert pytest.approx(neo_dict_data[0]["Ecc_"]) == 0.783616960
188 | assert pytest.approx(neo_dict_data[0]["Incl_deg"]) == 33.5207634
189 | assert pytest.approx(neo_dict_data[0]["LongAscNode_deg"]) == 278.480591
190 | assert pytest.approx(neo_dict_data[0]["ArgP_deg"]) == 75.9520569
191 | assert pytest.approx(neo_dict_data[0]["MeanAnom_deg"]) == 103.833748
192 | assert pytest.approx(neo_dict_data[0]["AbsMag_"]) == 21.0643673
193 |
194 |
195 | def test_Granvik2018Database():
196 | """
197 | Test the Granvik et al. (2018) SQLite database
198 |
199 | Returns
200 | -------
201 | None.
202 |
203 | """
204 |
205 | # Create the database
206 | granvik2018_sqlite = SolarY.neo.data.Granvik2018Database(new=True)
207 |
208 | # Check if a connection has been established and if a cursor has been set
209 | assert isinstance(granvik2018_sqlite.con, sqlite3.Connection)
210 | assert isinstance(granvik2018_sqlite.cur, sqlite3.Cursor)
211 |
212 | # Create the main table, get the first entry and verify the results
213 | granvik2018_sqlite.create()
214 | query_res_cur = granvik2018_sqlite.cur.execute(
215 | "SELECT ID, SemMajAxis_AU, ECC_ " "FROM main WHERE ID = 1"
216 | )
217 | query_res = query_res_cur.fetchone()
218 | assert query_res[1] == 2.57498121
219 | assert query_res[2] == 0.783616960
220 |
221 | # Create the dervied orbital elements and perform a verfication step
222 | granvik2018_sqlite.create_deriv_orb()
223 | query_res_cur = granvik2018_sqlite.cur.execute(
224 | "SELECT ID, Aphel_AU, Perihel_AU " "FROM main WHERE ID = 1"
225 | )
226 | query_res = query_res_cur.fetchone()
227 | assert query_res[1] == 4.592780157837321
228 | assert query_res[2] == 0.5571822621626783
229 |
230 | # Create the NEO class and perform a verfication step
231 | granvik2018_sqlite.create_neo_class()
232 | query_res_cur = granvik2018_sqlite.cur.execute(
233 | "SELECT ID, NEOClass " "FROM main WHERE ID = 1"
234 | )
235 | query_res = query_res_cur.fetchone()
236 | assert query_res[1] == 'Apollo'
237 |
238 | # Close the Granvik database
239 | granvik2018_sqlite.close()
240 |
241 |
242 | # test_download()
243 | # test_download_granvik2018()
244 | # test__get_neodys_neo_nr()
245 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (https://testrun.org/tox) is a tool for running tests
2 | # Install:
3 | # pip install tox
4 | # Run:
5 | # tox
6 | # tox -e docs
7 |
8 | [tox]
9 | minversion = 3.4.0
10 | envlist = py38,py39,mypy,bandit,flake8,doc8,pyroma,docs,build,coverage,black,blackdoc
11 | skip_missing_interpreters=true
12 |
13 | [testenv]
14 | deps =
15 | pytest
16 | pytest-mock
17 | -rrequirements.txt
18 | commands =
19 | pytest -vv --color=yes
20 |
21 | [testenv:coverage]
22 | setenv =
23 | PYTHONPATH = {toxinidir}
24 | deps =
25 | {[testenv]deps}
26 | pytest-cov
27 | pytest-html
28 | commands =
29 | pytest --cov=SolarY --cov-report=term-missing --cov-report=html --color=yes -vv
30 |
31 | [testenv:flake8]
32 | skip_install = true
33 | deps =
34 | flake8
35 | flake8-docstrings
36 | ; flake8-rst-docstrings
37 | pep8-naming
38 | flake8-bugbear
39 | flake8-isort
40 | ; flake8-builtins
41 | commands =
42 | flake8 SolarY
43 |
44 | [testenv:pre-commit]
45 | basepython = python3
46 | skip_install = true
47 | deps =
48 | pre-commit
49 | commands =
50 | pre-commit run --all-files --show-diff-on-failure
51 |
52 | [testenv:black]
53 | basepython = python3
54 | skip_install = true
55 | deps =
56 | black
57 | commands =
58 | # Use this locally and then use git status to see the difference.
59 | black --line-length=100 SolarY
60 | # If gitlab-ci is used, then use the one below
61 | # black --check SolarY
62 |
63 | [testenv:blackdoc]
64 | basepython = python3
65 | skip_install = true
66 | deps =
67 | blackdoc
68 | commands =
69 | # Use this locally and then use git status to see the difference.
70 | blackdoc SolarY
71 | # If gitlab-ci is used, then use the one below
72 | # blackdoc --check SolarY
73 |
74 | [testenv:pylint]
75 | skip_install = true
76 | deps =
77 | pylint>=1.8
78 | -rrequirements.txt
79 | commands =
80 | # C0103 => Module name "SolarY" doesn't conform to snake_case naming style (invalid-name)
81 | # W0511 => warns about TODO comments.
82 | pylint --disable=W0511,C0103 --ignore=_version.py --max-args=7 --output-format=colorized SolarY
83 |
84 | [testenv:detect_duplicate]
85 | skip_install = true
86 | deps =
87 | pylint>=1.8
88 | commands =
89 | pylint --disable=all --enable=duplicate-code --output-format=colorized SolarY
90 |
91 | [testenv:isort]
92 | skip_install = true
93 | deps =
94 | isort
95 | commands =
96 | isort SolarY
97 |
98 | [testenv:pyroma]
99 | skip_install = true
100 | deps =
101 | pyroma
102 | pygments
103 | commands =
104 | pyroma .
105 |
106 | [testenv:mypy]
107 | basepython = python3
108 | ;skip_install = true
109 | deps =
110 | mypy
111 | -rrequirements.txt
112 | commands =
113 | mypy --warn-unreachable --config mypy.ini SolarY
114 |
115 | [testenv:docs]
116 | setenv =
117 | PYTHONPATH = {toxinidir}
118 | ;skip_install = true
119 | deps =
120 | ; sphinx
121 | ; sphinx-rtd-theme
122 | ; sphinx-autodoc-typehints>=1.6.0
123 | ; sphinxcontrib-napoleon
124 | ;
125 | -rdocs/requirements-docs.txt
126 | commands =
127 | ; sphinx-build -W -E -b html docs/source docs/build/html
128 | sphinx-build -E -b html docs/source docs/build/html
129 |
130 | [testenv:serve-docs]
131 | basepython = python3
132 | skip_install = true
133 | changedir = docs/build/html
134 | deps =
135 | commands =
136 | python -m http.server {posargs}
137 |
138 | [testenv:build]
139 | skip_install = true
140 | deps =
141 | wheel
142 | setuptools
143 | commands =
144 | python setup.py bdist_wheel
145 |
146 | [testenv:readme]
147 | skip_install = true
148 | deps =
149 | readme_renderer
150 | commands =
151 | python setup.py check -r -s
152 |
153 | [testenv:doc8]
154 | skip_install = true
155 | deps =
156 | sphinx
157 | doc8
158 | commands =
159 | doc8 --ignore-path docs/source/*/generated docs/source
160 |
161 | [testenv:pydocstyle]
162 | skip_install = true
163 | deps =
164 | pydocstyle>=4
165 | commands =
166 | pydocstyle --convention numpy SolarY
167 | ; pydocstyle --convention numpy SolarY/instruments
168 |
169 | [testenv:manifest]
170 | skip_install = true
171 | deps =
172 | check-manifest
173 | commands =
174 | check-manifest
175 |
176 | [testenv:bandit]
177 | skip_install = true
178 | deps =
179 | bandit
180 | commands =
181 | bandit -r SolarY -f screen --exclude SolarY/_version.py
182 |
--------------------------------------------------------------------------------