├── .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 | --------------------------------------------------------------------------------