diff --git a/.gitignore b/.gitignore index 8e5bea2a..91272460 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist build eggs parts -bin var sdist develop-eggs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..68fd7028 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,8 @@ +exclude: '^$' +fail_fast: false +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.2.3 + hooks: + - id: flake8 + additional_dependencies: [flake8-docstrings, flake8-debugger, flake8-bugbear] diff --git a/bin/get_snos.py b/bin/get_snos.py new file mode 100644 index 00000000..511a55cb --- /dev/null +++ b/bin/get_snos.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 - 2019 Adam.Dybbroe + +# Author(s): + +# Adam.Dybbroe +# Nina HÃ¥kansson +# Erik Johansson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Getting SNOs for two configurable satellite platforms. + +SNO = Simultaneous Nadir Overpass: When two platforms sub-satellite track nadir +views cross each other in space and time. One can set a threshold in time +allowing the times to differ by up to a few minutes. + +""" + +import sys +from datetime import datetime, timedelta + +from pyorbital.orbital import Orbital +from pyorbital.sno_utils import get_config +from pyorbital.sno_utils import get_arc +from pyorbital.sno_utils import get_tle +from pyorbital.sno_utils import get_sno_point +import logging + +LOG = logging.getLogger('snos') + +handler = logging.StreamHandler(sys.stderr) +handler.setLevel(0) +LOG.setLevel(0) +LOG.addHandler(handler) + + +def get_arguments(): + """Get the comman line arguments required to run the script.""" + import argparse + + parser = argparse.ArgumentParser(description='Calculate SNOS between two satellite platforms') + parser.add_argument("-s", "--start-datetime", + required=True, + dest="start_datetime", + type=str, + default=None, + help="The datetime string corresponding to the start time of when SNOS should be calculated") + parser.add_argument("-e", "--end-datetime", + required=True, + dest="end_datetime", + type=str, + default=None, + help="The datetime string corresponding to the end time of when SNOS should be calculated") + parser.add_argument("-t", "--time-window", + required=True, + dest="time_window", + type=str, + default=None, + help=("The time window in number of minutes - the maximum time allowed between " + + "the two SNO observations")) + parser.add_argument("-p", "--platform-name", + required=True, + dest="platform_name", + type=str, + default=None, + help="The name of the satellite platform") + parser.add_argument("-c", "--configfile", + required=True, + dest="configfile", + type=str, + default=None, + help="The path to the configuration file") + parser.add_argument("-l", "--log-file", dest="log", + type=str, + default=None, + help="The file to log to (stdout per default).") + + args = parser.parse_args() + return args + + +def main(): + """Find SNOs for the two platforms within the time period given.""" + args = get_arguments() + conf = get_config(args.configfile) + + station = {} + station['lon'] = conf['station']['longitude'] + station['lat'] = conf['station']['latitude'] + station['alt'] = conf['station']['altitude'] + + ref_platform_name = conf['reference_platform']['name'] + + tle_dirs = conf['tle-dirs'] + tle_file_format = conf['tle-file-format'] + platform_id = args.platform_name + minthr = int(args.time_window) + time_start = datetime.strptime(args.start_datetime, "%Y%m%d") + time_end = datetime.strptime(args.end_datetime, "%Y%m%d") + + dtime = timedelta(seconds=60 * minthr * 2.0) + timestep = timedelta(seconds=60 * minthr * 1.0) + delta_t = dtime + + tobj = time_start + tle_ref_pltfrm = None + tle_cmp_platform = None + tobj_tmp = time_start + while tobj < time_end: + if not tle_ref_pltfrm or abs(tle_ref_pltfrm.epoch.astype(datetime) - tobj) > timedelta(days=1): + tle_ref_pltfrm = get_tle(tle_dirs, tle_file_format, ref_platform_name, tobj) + if (not tle_cmp_platform or + abs(tle_cmp_platform.epoch.astype(datetime) - tobj) > timedelta(days=2)): + tle_cmp_platform = get_tle(tle_dirs, tle_file_format, platform_id, tobj) + + ref_pltfrm = Orbital(ref_platform_name, + line1=tle_ref_pltfrm.line1, + line2=tle_ref_pltfrm.line2) + cmp_platform = Orbital(platform_id, + line1=tle_cmp_platform.line1, + line2=tle_cmp_platform.line2) + + arc_ref_pltfrm = get_arc(tobj, timestep, ref_pltfrm) + arc_cmp_platform = get_arc(tobj, timestep, cmp_platform) + + if arc_ref_pltfrm and arc_cmp_platform: + if arc_ref_pltfrm.intersects(arc_cmp_platform): + # If the two sub-satellite tracks of the overpasses intersects + # get the sub-satellite position and time where they cross, + # and determine if the time deviation is smaller than the require threshold: + sno = get_sno_point(platform_id, cmp_platform, ref_pltfrm, + tobj, delta_t, arc_ref_pltfrm, + arc_cmp_platform, station, minthr) + + if sno: + print(" " + + str(sno['maxt_ref_pltfrm'].strftime("%Y-%m-%d %H:%M, ")) + + "%5.1f," % sno['ref_pltfrmsec'] + " "*5 + + str(sno['maxt'].strftime("%Y-%m-%d %H:%M, ")) + + "%5.1f," % sno['sec'] + + # " imager-orbit, %d, " % (sno['orbit_nr'] + 1) + + " %d, " % (sno['orbit_nr'] + 1) + + " "*6 + "%7.2f, %7.2f," % (sno['point'][1], sno['point'][0]) + + " " + "%4.1f," % abs(sno['tdmin']) + " " + str(sno['is_within_antenna_horizon']) + ) + + else: + LOG.error("Failed getting the track-segments") + + tobj = tobj + dtime + if tobj - tobj_tmp > timedelta(days=1): + tobj_tmp = tobj + LOG.debug(tobj_tmp.strftime("%Y-%m-%d")) + + +if __name__ == "__main__": + main() diff --git a/doc/source/api.rst b/doc/source/api.rst new file mode 100644 index 00000000..ad5c7a5e --- /dev/null +++ b/doc/source/api.rst @@ -0,0 +1,30 @@ +The :mod:`pyorbital` API +========================= + +Orbital computations +~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: pyorbital.orbital + :members: + :undoc-members: + +TLE handling +~~~~~~~~~~~~ + +.. automodule:: pyorbital.tlefile + :members: + :undoc-members: + +Astronomical computations +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: pyorbital.astronomy + :members: + :undoc-members: + +Simultaneous Nadir Overpass computations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: pyorbital.sno_utils + :members: + :undoc-members: diff --git a/doc/source/astronomy.rst b/doc/source/astronomy.rst new file mode 100644 index 00000000..52d93dad --- /dev/null +++ b/doc/source/astronomy.rst @@ -0,0 +1,11 @@ +Computing astronomical parameters +--------------------------------- +The astronomy module enables computation of certain parameters of interest for satellite remote sensing for instance the Sun-zenith angle: + + >>> from pyorbital import astronomy + >>> from datetime import datetime + >>> utc_time = datetime(2012, 5, 15, 15, 45) + >>> lon, lat = 12, 56 + >>> astronomy.sun_zenith_angle(utc_time, lon, lat) + 62.685986438071602 + diff --git a/doc/source/index.rst b/doc/source/index.rst index 39d7f504..59477740 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,117 +3,27 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Pyorbital -========= +==================================== +Welcome to Pyorbital's documentation +==================================== Pyorbital is a python package to compute orbital parameters for satellites from TLE files as well as astronomical parameters of interest for satellite remote sensing. Currently pyorbital only supports low earth orbit satellites. -Installation ------------- -Pyorbital comes with a file platforms.txt that maps satellite name to NORAD identifier. -This file needs to be copied to the appropriate satpy etc directory ($PPP_CONFIG_DIR). -It is wise to check it contains your satellites of interest. The NORAD identifier can -be found as the first number of each line in the Two-Line Elements (eg. from celestrak). - -TLE files ---------- -Pyorbital has a module for parsing NORAD TLE-files - - >>> from pyorbital import tlefile - >>> tle = tlefile.read('noaa 18', '/path/to/my/tle_file.txt') - >>> tle.inclination - 99.043499999999995 - -If no path is given pyorbital tries to read the earth observation TLE-files from celestrak.com - -Computing satellite position ----------------------------- -The orbital module enables computation of satellite position and velocity at a specific time: - - >>> from pyorbital.orbital import Orbital - >>> from datetime import datetime - >>> # Use current TLEs from the internet: - >>> orb = Orbital("Suomi NPP") - >>> now = datetime.utcnow() - >>> # Get normalized position and velocity of the satellite: - >>> orb.get_position(now) - (array([-0.20015267, 0.09001458, 1.10686756]), - array([ 0.06148495, 0.03234914, 0.00846805])) - >>> # Get longitude, latitude and altitude of the satellite: - >>> orb.get_lonlatalt(now) - (40.374855865574951, 78.849923885700363, 839.62504115338368) - - -Use actual TLEs to increase accuracy ------------------------------------- - - >>> from pyorbital.orbital import Orbital - >>> from datetime import datetime - >>> orb = Orbital("Suomi NPP") - >>> dtobj = datetime(2015,2,7,3,0) - >>> orb.get_lonlatalt(dtobj) - (152.11564698762811, 20.475251739329622, 829.37355785502211) - -But since we are interested in knowing the position of the Suomi-NPP more than -two and half years from now (September 26, 2017) we can not rely on the current -TLEs, but rather need a TLE closer to the time of interest: - - >>> snpp = Orbital('Suomi NPP', tle_file='/data/lang/satellit/polar/orbital_elements/TLE/201502/tle-20150207.txt') - >>> snpp.get_lonlatalt(dtobj) - (105.37373804512762, 79.160752404540133, 838.94605490133154) - -If we take a TLE from one week earlier we get a slightly different result: - - >>> snpp = Orbital('Suomi NPP', tle_file='/data/lang/satellit/polar/orbital_elements/TLE/201501/tle-20150131.txt') - >>> snpp.get_lonlatalt(dtobj) - (104.1539184988462, 79.328272480878141, 838.81555967963391) - - - -Computing astronomical parameters ---------------------------------- -The astronomy module enables computation of certain parameters of interest for satellite remote sensing for instance the Sun-zenith angle: - - >>> from pyorbital import astronomy - >>> from datetime import datetime - >>> utc_time = datetime(2012, 5, 15, 15, 45) - >>> lon, lat = 12, 56 - >>> astronomy.sun_zenith_angle(utc_time, lon, lat) - 62.685986438071602 - -API ---- - -Orbital computations -~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: pyorbital.orbital - :members: - :undoc-members: - -TLE handling -~~~~~~~~~~~~ - -.. automodule:: pyorbital.tlefile - :members: - :undoc-members: - -Astronomical computations -~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. automodule:: pyorbital.astronomy - :members: - :undoc-members: - - -.. Contents: - .. toctree:: - :maxdepth: 2 - Indices and tables - ================== - * :ref:`genindex` - * :ref:`modindex` - * :ref:`search` +.. toctree:: + :maxdepth: 2 + + install + tlefiles + sat_position + astronomy + snos + api + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/install.rst b/doc/source/install.rst new file mode 100644 index 00000000..aea07592 --- /dev/null +++ b/doc/source/install.rst @@ -0,0 +1,41 @@ +========================= +Installation Instructions +========================= + +Note +---- +Pyorbital comes with a file platforms.txt that maps satellite name to NORAD identifier. +This file needs to be copied to the appropriate Satpy `etc` directory ($PPP_CONFIG_DIR). +It is wise to check it contains your satellites of interest. The NORAD identifier can +be found as the first number of each line in the Two-Line Elements (eg. from celestrak). + +Pip-based Installation +====================== + +Pyorbital is available from the Python Packaging Index (PyPI). A sandbox +environment for `pyorbital` can be created using +`Virtualenv `_. + +To install the `pyorbital` package and the python dependencies: + +.. code-block:: bash + + $ pip install pyorbital + + +Conda-based Installation +======================== + +Starting with version 1.3.1, Pyorbital is available from the conda-forge channel. If +you have not configured your conda environment to search conda-forge already +then do: + +.. code-block:: bash + + $ conda config --add channels conda-forge + +Then to install Pyorbital in to your current environment run: + +.. code-block:: bash + + $ conda install pyorbital diff --git a/doc/source/sat_position.rst b/doc/source/sat_position.rst new file mode 100644 index 00000000..ac7071bd --- /dev/null +++ b/doc/source/sat_position.rst @@ -0,0 +1,44 @@ + +Computing satellite position +---------------------------- +The orbital module enables computation of satellite position and velocity at a specific time: + + >>> from pyorbital.orbital import Orbital + >>> from datetime import datetime + >>> # Use current TLEs from the internet: + >>> orb = Orbital("Suomi NPP") + >>> now = datetime.utcnow() + >>> # Get normalized position and velocity of the satellite: + >>> orb.get_position(now) + (array([-0.20015267, 0.09001458, 1.10686756]), + array([ 0.06148495, 0.03234914, 0.00846805])) + >>> # Get longitude, latitude and altitude of the satellite: + >>> orb.get_lonlatalt(now) + (40.374855865574951, 78.849923885700363, 839.62504115338368) + + +Use actual TLEs to increase accuracy +------------------------------------ + + >>> from pyorbital.orbital import Orbital + >>> from datetime import datetime + >>> orb = Orbital("Suomi NPP") + >>> dtobj = datetime(2015,2,7,3,0) + >>> orb.get_lonlatalt(dtobj) + (152.11564698762811, 20.475251739329622, 829.37355785502211) + +But since we are interested in knowing the position of the Suomi-NPP more than +two and half years from now (September 26, 2017) we can not rely on the current +TLEs, but rather need a TLE closer to the time of interest: + + >>> snpp = Orbital('Suomi NPP', tle_file='/data/lang/satellit/polar/orbital_elements/TLE/201502/tle-20150207.txt') + >>> snpp.get_lonlatalt(dtobj) + (105.37373804512762, 79.160752404540133, 838.94605490133154) + +If we take a TLE from one week earlier we get a slightly different result: + + >>> snpp = Orbital('Suomi NPP', tle_file='/data/lang/satellit/polar/orbital_elements/TLE/201501/tle-20150131.txt') + >>> snpp.get_lonlatalt(dtobj) + (104.1539184988462, 79.328272480878141, 838.81555967963391) + + diff --git a/doc/source/snos.rst b/doc/source/snos.rst new file mode 100644 index 00000000..036c5ce1 --- /dev/null +++ b/doc/source/snos.rst @@ -0,0 +1,49 @@ +Finding Simultaneous Nadir Overpasses for two different platforms +------------------------------------------------------------------ + +Pyorbital facilitates the identification and calculation of simultaneous nadir +overpasses (SNOs) for two different platforms. This is for example useful when +you want to colocate observations in time and space and where the viewing +conditions are the same (nadir viewing) for both platforms. One example is when +validating clous parameters derived from Imager sensors like VIIRS, AVHRR or +MODIS against the active nadir viewing sensors of the Calipso and CloudSat in +the A-train constellation. + +**Example usage:** + +Below is an example searching for SNO points between the EOS-Aqua and the +Metop-B platforms. We allow a maximum of 10 minutes between the two +observations where the two nadir viewsare crossing. Thus here *simultaneous* +means within 10 minutes. In the example we limit the time period to January +2015 and we provide a directory with daily tle-files for the entire month. + +.. code-block:: bash + + %> python ./bin/get_snos.py -s 20150101 -e 20150131 -t 10 -p Metop-B -c ./examples/snos.yaml + +A few basic parameters need to be defined in the configuration yaml file **snos.yaml**: + +.. code-block:: bash + + station: + longitude: 16.1465 + latitude: 58.5780 + altitude: 0.03 + + tle-dirs: + - /home/a000680/data/tles/2015/201501 + + platform_names: + - NOAA-18 + - NOAA-19 + - NOAA-20 + - Suomi-NPP + - Metop-B + - Metop-C + + tle-file-format: 'tle-%Y%m%d.txt' + + reference_platform: + name: EOS-Aqua + + diff --git a/doc/source/tlefiles.rst b/doc/source/tlefiles.rst new file mode 100644 index 00000000..966a86ea --- /dev/null +++ b/doc/source/tlefiles.rst @@ -0,0 +1,11 @@ +TLE files +--------- +Pyorbital has a module for parsing NORAD TLE-files + + >>> from pyorbital import tlefile + >>> tle = tlefile.read('noaa 18', '/path/to/my/tle_file.txt') + >>> tle.inclination + 99.043499999999995 + +If no path is given pyorbital tries to read the earth observation TLE-files from celestrak.com + diff --git a/examples/snos.yaml_template b/examples/snos.yaml_template new file mode 100644 index 00000000..a99851e4 --- /dev/null +++ b/examples/snos.yaml_template @@ -0,0 +1,31 @@ +# The position of a Direct Readout station of interest. It is not necessary to +# change this in order to calculate the SNOs, but if/when set the sno-script +# will mark those crossings where the (imager) satellite is within the +# reception horizon (makred=True). +station: + longitude: 16.1465 + latitude: 58.5780 + altitude: 0.03 + +# Here a list of directories can be given, to determine where to look up the +# TLE files covering the time period of interest. +tle-dirs: + - /path/to/tle/files + +# The platform names supported. +platform_names: + - NOAA-18 + - NOAA-19 + - NOAA-20 + - Suomi-NPP + - Metop-B + - Metop-C + +# The format of tle filenames. +tle-file-format: 'tle-%Y%m%d.txt' + +# Reference platform is usually Calipso or Aqua for a proxy of the A-train +# Calipso/Cloudsat +reference_platform: + name: EOS-Aqua + \ No newline at end of file diff --git a/pyorbital/sno_utils.py b/pyorbital/sno_utils.py new file mode 100644 index 00000000..4e51c6e9 --- /dev/null +++ b/pyorbital/sno_utils.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Pytroll + +# Author(s): + +# Adam.Dybbroe + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Utility functions to find simultaneous nadir overpasses (SNOs).""" + +import os +from datetime import timedelta +from pyorbital.tlefile import Tle +from pyresample.spherical_geometry import Coordinate, Arc +import math +import logging +import yaml +from yaml import SafeLoader + +try: + # python 3.3+ + from collections.abc import Mapping +except ImportError: + # deprecated (above can't be done in 2.7) + from collections import Mapping + +LOG = logging.getLogger(__name__) + +TLE_BUFFER = {} +TLE_SATNAME = {'Suomi-NPP': 'SUOMI NPP', + 'NOAA-20': 'NOAA 20', + 'EOS-Aqua': 'AQUA', + 'Metop-B': 'METOP-B', + 'NOAA-19': 'NOAA 19', + 'NOAA-18': 'NOAA 18'} + + +class NoTleFile(Exception): + """Exception to catch missing TLE file.""" + + pass + + +def _recursive_dict_update(d, u): + """Recursive dictionary update. + + Copied from: + + http://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth + + """ + for k, v in u.items(): + if isinstance(v, Mapping): + r = _recursive_dict_update(d.get(k, {}), v) + d[k] = r + else: + d[k] = u[k] + return d + + +def get_config(configfile): + """Get the configuration from file. + + :configfile: The file path of the yaml configuration file. + + :return: A configuration dictionary. + + """ + config = {} + with open(configfile, 'r') as fp_: + config = _recursive_dict_update(config, yaml.load(fp_, Loader=SafeLoader)) + + return config + + +def _get_tle_file(tledirs, tle_file_format, timestamp): + # Find a not too old TLE file + for path in tledirs: + if os.path.isdir(path): + for idx in range(5): + dt_obj = timestamp - timedelta(days=idx) + fname = os.path.join(path, dt_obj.strftime(tle_file_format)) + if os.path.isfile(fname): + LOG.info("Found TLE file: '%s'" % fname) + return fname + raise NoTleFile("Found no TLE file close in time to " + + str(timestamp.strftime(tle_file_format))) + + +def get_tle(tle_dirs, tle_file_format, platform, timestamp=None): + """Get the tle from file, if not loaded already. + + :tle_dirs: A list of paths where tle-files are located + :tle_file_format: The tle-file format pattern + :platorm: Satellite platform name + + :return: A pyorbital Tle object + + """ + stamp = platform + timestamp.strftime('-%Y%m%d') + try: + tle = TLE_BUFFER[stamp] + except KeyError: + tle = Tle(TLE_SATNAME.get(platform, platform), + _get_tle_file(tle_dirs, tle_file_format, timestamp)) + TLE_BUFFER[stamp] = tle + return tle + + +def get_arc(timeobj, delta_t, sat): + """Get the arc on the geoid of the sub-satellit track over a small time interval. + + It get's the arc defining the sub-satellite track from start time 'timeobj' + to start time 'timeobj' plus a time step 'delta_t'. + + :timeobj: Start time (datetime object) + :delta_t: Time step to add to the start time + :sat: Orbital object for the satellite platform + + :return: A Pyresample spherical geometry arc object + + """ + # Get the start and end positions: + pos_start = sat.get_lonlatalt(timeobj - delta_t) + pos_end = sat.get_lonlatalt(timeobj + delta_t) + + coord_start = Coordinate(lon=pos_start[0], lat=pos_start[1]) + coord_end = Coordinate(lon=pos_end[0], lat=pos_end[1]) + + return Arc(coord_start, coord_end) + + +def get_sno_point(platform_id, cmp_platform, ref_platform, tobj, delta_t, + arc_ref_pltfrm, arc_cmp_platform, station, minute_thr): + """Get the SNO point if there is any. + + If the two sub-satellite tracks of the overpasses intersects + get the sub-satellite position and time where they cross, + and determine if the time deviation is smaller than the require threshold: + """ + intersect = arc_ref_pltfrm.intersection(arc_cmp_platform) + point = (math.degrees(intersect.lon), + math.degrees(intersect.lat)) + + nextp = cmp_platform.get_next_passes(tobj-delta_t, + 2, + point[0], + point[1], + 0) + if len(nextp) > 0: + riset, fallt, maxt = nextp[0] + else: + LOG.warning("No next passes found for " + + platform_id + "! " + str(nextp)) + return None + + nextp = ref_platform.get_next_passes(tobj-delta_t, + 2, + point[0], + point[1], + 0) + if len(nextp) > 0: + riset, fallt, maxt_ref_pltfrm = nextp[0] + else: + LOG.warning("No next passes found! " + str(nextp)) + return None + + # Get observer look from the specified DR station to the satellite when it + # is at zenith over the SNO point: + azi, elev = cmp_platform.get_observer_look(maxt, + station['lon'], + station['lat'], + station['alt']) + + is_within_antenna_horizon = (elev > 0.0) + + ref_pltfrmsec = (int(maxt_ref_pltfrm.strftime("%S")) + + int(maxt_ref_pltfrm.strftime("%f"))/1000000.) + sec = (int(maxt.strftime("%S")) + + int(maxt.strftime("%f"))/1000000.) + + tdelta = (maxt_ref_pltfrm - maxt) + tdmin = (tdelta.seconds + tdelta.days * 24*3600) / 60. + if abs(tdmin) < minute_thr: + orbit_nr = cmp_platform.get_orbit_number(maxt) + + result = {} + result['maxt'] = maxt + result['maxt_ref_pltfrm'] = maxt_ref_pltfrm + result['ref_pltfrmsec'] = ref_pltfrmsec + result['sec'] = sec + result['is_within_antenna_horizon'] = is_within_antenna_horizon + result['tdmin'] = tdmin + result['orbit_nr'] = orbit_nr + result['point'] = point + + return result + else: + return None diff --git a/pyorbital/tests/__init__.py b/pyorbital/tests/__init__.py index c41997e2..0d39859c 100644 --- a/pyorbital/tests/__init__.py +++ b/pyorbital/tests/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2014 Martin Raspaud +# Copyright (c) 2014, 2019 Martin Raspaud # Author(s): @@ -19,15 +19,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""The tests package.""" +"""Test the pyorbital package.""" from pyorbital.tests import (test_aiaa, test_tlefile, test_orbital, - test_astronomy, test_geoloc) + test_astronomy, test_geoloc, test_snos) import unittest def suite(): - """The global test suite.""" + """Test the Pyorbital package.""" mysuite = unittest.TestSuite() # Test the documentation strings # mysuite.addTests(doctest.DocTestSuite(image)) @@ -37,6 +37,7 @@ def suite(): mysuite.addTests(test_orbital.suite()) mysuite.addTests(test_astronomy.suite()) mysuite.addTests(test_geoloc.suite()) + mysuite.addTests(test_snos.suite()) return mysuite diff --git a/pyorbital/tests/test_snos.py b/pyorbital/tests/test_snos.py new file mode 100644 index 00000000..c1125209 --- /dev/null +++ b/pyorbital/tests/test_snos.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Adam.Dybbroe + +# Author(s): + +# Adam.Dybbroe + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +"""Test the functions to find SNOs from two different satellite platforms.""" + +from pyorbital.orbital import Orbital +from pyorbital.sno_utils import get_arc +from pyorbital.sno_utils import get_sno_point +import sys +from datetime import datetime, timedelta + +if sys.version_info < (2, 7): + import unittest2 as unittest +else: + import unittest + + +class GetSnosTest(unittest.TestCase): + """Test getting snos.""" + + def setUp(self): + """Set up the test.""" + self.platform_id = 'Metop-B' + self.orb_ref1 = Orbital('EOS-Aqua', + line1='1 27424U 02022A 14365.22903259 .00000847 00000-0 19796-3 0 2320', + line2='2 27424 98.2234 303.5330 0001197 87.2691 48.8815 14.57121895673369') + self.orb_cmp1 = Orbital('Metop-B', + line1='1 38771U 12049A 14364.85213337 .00000110 00000-0 70147-4 0 6893', + line2='2 38771 98.7220 62.0425 0000591 33.5587 96.4312 14.21479121118503') + self.tobj1 = datetime(2015, 1, 1, 0, 20) + self.delta_t = timedelta(seconds=1200) + self.timestep = timedelta(seconds=600) + self.minthr = 10 + self.station = {'lon': 16.1465, 'lat': 58.578, 'alt': 0.03} + + self.orb_ref2 = Orbital('EOS-Aqua', + line1='1 27424U 02022A 15003.03711450 .00000723 00000-0 17040-3 0 2344', + line2='2 27424 98.2239 306.3049 0001165 87.8737 9.7700 14.57125023673775') + self.orb_cmp2 = Orbital('Metop-B', + line1='1 38771U 12049A 15002.73354670 .00000116 00000-0 73308-4 0 6917', + line2='2 38771 98.7211 64.8896 0000438 20.9656 85.8395 14.21479960118911') + self.tobj2 = datetime(2015, 1, 3, 17, 20) + + def test_get_sno_point(self): + """Test getting a SNO point.""" + arc_ref_pltfrm = get_arc(self.tobj1, self.timestep, self.orb_ref1) + arc_cmp_pltfrm = get_arc(self.tobj1, self.timestep, self.orb_cmp1) + sno = get_sno_point(self.platform_id, self.orb_cmp1, self.orb_ref1, + self.tobj1, self.delta_t, arc_ref_pltfrm, + arc_cmp_pltfrm, self.station, self.minthr) + + expected = {'maxt': datetime(2015, 1, 1, 0, 12, 0, 506020), + 'maxt_ref_pltfrm': datetime(2015, 1, 1, 0, 10, 36, 45923), + 'ref_pltfrmsec': 36.045923, + 'sec': 0.50602, + 'is_within_antenna_horizon': False, + 'tdmin': -1.4166666666666667, + 'orbit_nr': 11866, + 'point': (-9.057282984227129, -74.5159273899483)} + + self.assertAlmostEqual(expected['sec'], sno['sec']) + self.assertAlmostEqual(expected['ref_pltfrmsec'], sno['ref_pltfrmsec']) + self.assertAlmostEqual(expected['tdmin'], sno['tdmin']) + self.assertTrue(expected['is_within_antenna_horizon'] == sno['is_within_antenna_horizon']) + self.assertEqual(expected['orbit_nr'], sno['orbit_nr']) + self.assertEqual(expected['maxt'], sno['maxt']) + self.assertEqual(expected['maxt_ref_pltfrm'], sno['maxt_ref_pltfrm']) + self.assertAlmostEqual(expected['point'][0], sno['point'][0]) + self.assertAlmostEqual(expected['point'][1], sno['point'][1]) + + arc_ref_pltfrm = get_arc(self.tobj2, self.timestep, self.orb_ref2) + arc_cmp_pltfrm = get_arc(self.tobj2, self.timestep, self.orb_cmp2) + sno = get_sno_point(self.platform_id, self.orb_cmp2, self.orb_ref2, + self.tobj2, self.delta_t, arc_ref_pltfrm, + arc_cmp_pltfrm, self.station, self.minthr) + + expected = {'maxt': datetime(2015, 1, 3, 17, 13, 54, 493133), + 'maxt_ref_pltfrm': datetime(2015, 1, 3, 17, 16, 46, 332581), + 'ref_pltfrmsec': 46.332581, + 'sec': 54.493133, + 'is_within_antenna_horizon': False, + 'tdmin': 2.85, + 'orbit_nr': 11905, + 'point': (-82.65813724524172, 76.0882567980114)} + + self.assertAlmostEqual(expected['sec'], sno['sec']) + self.assertAlmostEqual(expected['ref_pltfrmsec'], sno['ref_pltfrmsec']) + self.assertAlmostEqual(expected['tdmin'], sno['tdmin']) + self.assertTrue(expected['is_within_antenna_horizon'] == sno['is_within_antenna_horizon']) + self.assertEqual(expected['orbit_nr'], sno['orbit_nr']) + self.assertEqual(expected['maxt'], sno['maxt']) + self.assertEqual(expected['maxt_ref_pltfrm'], sno['maxt_ref_pltfrm']) + self.assertAlmostEqual(expected['point'][0], sno['point'][0]) + self.assertAlmostEqual(expected['point'][1], sno['point'][1]) + + +def suite(): + """Run the test suite for testing finding SNOs.""" + loader = unittest.TestLoader() + mysuite = unittest.TestSuite() + mysuite.addTest(loader.loadTestsFromTestCase(GetSnosTest)) + + return mysuite diff --git a/setup.py b/setup.py index e0a9cdb0..64a332e6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2011-2014, 2018 +# Copyright (c) 2011-2014, 2018, 2019 # Author(s): @@ -19,6 +19,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +"""Setup file for Pyorbital.""" import os from setuptools import setup, find_packages @@ -28,8 +29,8 @@ version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), description='Orbital parameters and astronomical computations in Python', - author='Martin Raspaud, Esben S. Nielsen', - author_email='martin.raspaud@smhi.se', + author='The Pytroll Team', + author_email='pytroll@googlegroups.com', classifiers=["Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", "License :: OSI Approved :: GNU General Public License v3 " +