From 82af0355dcaf444ddc89f9fdbaae4f10bb0b0dba Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 15:00:56 +0100 Subject: [PATCH 01/12] Update ReadTheDocs configuration --- .readthedocs.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c8d9e14..7e589b0 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,16 +1,20 @@ version: 2 build: - image: latest + os: ubuntu-24.04 + tools: + python: "3.11" python: - version: 3.8 install: - - requirements: docs/requirements.txt - - method: pip - path: . + - requirements: docs/requirements.txt + - method: pip + path: . sphinx: builder: html configuration: docs/conf.py fail_on_warning: true + +formats: + - pdf From 17ff4c448ad6d1d82ae79faaab2adf18fc29b0fb Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 15:04:09 +0100 Subject: [PATCH 02/12] Fix python version in setup.py classifier --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e3f1385..1deb0fb 100644 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.11', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Astronomy', ], From 5c652528a7b12666520b70075264f63b34780c93 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 15:11:41 +0100 Subject: [PATCH 03/12] Update README --- README.rst | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 572083d..bc01c75 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ -Python package for NAIF WebGeoCalc API -====================================== +๐Ÿ Python package for NAIF WebGeoCalc API +========================================= |Docs| |Build| |Coverage| |CodeFactor| @@ -47,8 +47,8 @@ calculations through this API. .. _Documentation: https://wgc2.jpl.nasa.gov:8443/webgeocalc/documents/api-info.html .. _`JavaScript examples`: https://wgc2.jpl.nasa.gov:8443/webgeocalc/example/perform-calculation.html -Note the user -------------- +โš ๏ธ Caution +---------- `WebGeoCalc`_ is not design to handle heavy calculation. If you need to make intensive queries, use `Spiceypy`_ or `SpiceMiner`_ @@ -59,8 +59,8 @@ Note the user .. _`SpiceMiner`: https://github.com/DaRasch/spiceminer -Install -------- +๐Ÿ“ฆ Install +---------- With ``pip``: .. code:: bash @@ -68,8 +68,8 @@ With ``pip``: $ pip install webgeocalc -Usage ------ +๐Ÿš€ Usage +-------- .. code:: python @@ -155,8 +155,9 @@ More details can be found in the `docs`_ and in the `Jupyter Notebooks`_. .. _`docs`: https://webgeocalc.readthedocs.io/en/stable/calculation.html .. _`Jupyter Notebooks`: https://nbviewer.jupyter.org/github/seignovert/python-webgeocalc/blob/main/examples/calculation.ipynb -Command Line Interface (cli) ----------------------------- + +โš™๏ธ Command Line Interface (cli) +------------------------------- The webgeocalc API can be call directly from the command line interface: @@ -212,8 +213,8 @@ More examples can be found in here_. .. _here: https://webgeocalc.readthedocs.io/en/stable/cli.html -Local development and testing ------------------------------ +๐Ÿงช Local development and testing +-------------------------------- Setup: @@ -244,7 +245,8 @@ Docs: sphinx-build docs docs/_build --color -W -bdoctest -Disclaimer ----------- +๐Ÿ“ฃ Disclaimer +------------- + This project is not supported or endorsed by either JPL, NAIF or NASA. The code is provided *"as is"*, use at your own risk. From 9d0df22a0227f786b6742874051ecb21a7eb7d93 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 19:10:27 +0100 Subject: [PATCH 04/12] Add missing entrypoints --- webgeocalc/__init__.py | 23 ++++++++++++++++++----- webgeocalc/vars.py | 14 ++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/webgeocalc/__init__.py b/webgeocalc/__init__.py index f5b3c51..d55aa5e 100644 --- a/webgeocalc/__init__.py +++ b/webgeocalc/__init__.py @@ -15,17 +15,30 @@ 'API', 'JPL_API', 'ESA_API', + 'Calculation', + 'StateVector', 'AngularSeparation', 'AngularSize', - 'Calculation', 'FrameTransformation', - 'GFCoordinateSearch', 'IlluminationAngles', - 'OsculatingElements', - 'StateVector', - 'SubObserverPoint', + # 'PhaseAngle', + # 'PointingDirection', 'SubSolarPoint', + 'SubObserverPoint', 'SurfaceInterceptPoint', + # 'TangentPoint', + 'OsculatingElements', + 'GFCoordinateSearch', + # 'GFAngularSeparationSearch' + # 'GFDistanceSearch' + # 'GFSubPointSearch' + # 'GFOccultationSearch' + # 'GFSurfaceInterceptPointSearch' + # 'GFTargetInInstrumentFovSearch' + # 'GFRayInFovSearch' + # 'GFRangeRateSearch' + # 'GFPhaseAngleSearch' + # 'GFIlluminationAnglesSearch' 'TimeConversion', '__version__', ] diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index b21adbf..603c316 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -15,20 +15,26 @@ 'STATE_VECTOR', 'ANGULAR_SEPARATION', 'ANGULAR_SIZE', - 'SUB_OBSERVER_POINT', - 'SUB_SOLAR_POINT', + 'FRAME_TRANSFORMATION', 'ILLUMINATION_ANGLES', + # 'PHASE_ANGLE', + # 'POINTING_DIRECTION', + 'SUB_SOLAR_POINT', + 'SUB_OBSERVER_POINT', 'SURFACE_INTERCEPT_POINT', + # 'TANGENT_POINT', 'OSCULATING_ELEMENTS', - 'FRAME_TRANSFORMATION', 'GF_COORDINATE_SEARCH', 'GF_ANGULAR_SEPARATION_SEARCH', 'GF_DISTANCE_SEARCH', 'GF_SUB_POINT_SEARCH', 'GF_OCCULTATION_SEARCH', - 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', 'GF_SURFACE_INTERCEPT_POINT_SEARCH', + 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', 'GF_RAY_IN_FOV_SEARCH', + 'GF_RANGE_RATE_SEARCH', + 'GF_PHASE_ANGLE_SEARCH', + 'GF_ILLUMINATION_ANGLES_SEARCH', 'TIME_CONVERSION', ], 'TIME_SYSTEM': [ From 7ea856d9448179f1864d5c16dec5687a03fa76a2 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 19:23:18 +0100 Subject: [PATCH 05/12] Add __eq__ method to compare payload directly with a dict --- tests/test_direction.py | 20 ++++++++++---------- tests/test_payload.py | 5 ++++- webgeocalc/payload.py | 3 +++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/test_direction.py b/tests/test_direction.py index 7ec80d7..97061ff 100644 --- a/tests/test_direction.py +++ b/tests/test_direction.py @@ -16,7 +16,7 @@ def test_direction_position_with_shape(): target='MARS', shape='POINT', observer='EARTH', - ).payload == { + ) == { 'directionType': 'POSITION', 'target': 'MARS', 'shape': 'POINT', @@ -34,7 +34,7 @@ def test_direction_position_without_shape(): observer='EARTH', aberration_correction='LT+S', anti_vector_flag='TRUE', # Uppercase - ).payload == { + ) == { 'directionType': 'POSITION', 'target': 'MARS', 'observer': 'EARTH', @@ -99,7 +99,7 @@ def test_direction_velocity(): observer='EARTH', aberration_correction='XCN+S', anti_vector_flag='true', # Lowercase - ).payload == { + ) == { 'directionType': 'VELOCITY', 'target': 'MARS', 'referenceFrame': 'ITRF93', @@ -143,7 +143,7 @@ def test_direction_vector_instrument_boresight(): direction_type='VECTOR', direction_vector_type='INSTRUMENT_BORESIGHT', direction_instrument='CASSINI_ISS_NAC', - ).payload == { + ) == { 'directionType': 'VECTOR', 'directionVectorType': 'INSTRUMENT_BORESIGHT', 'directionInstrument': 'CASSINI_ISS_NAC', @@ -158,7 +158,7 @@ def test_direction_vector_instrument_boresight(): direction_vector_type='INSTRUMENT_BORESIGHT', direction_instrument='CASSINI_ISS_NAC', aberration_correction='CN' - ).payload == { + ) == { 'directionType': 'VECTOR', 'observer': 'CASSINI', 'directionVectorType': 'INSTRUMENT_BORESIGHT', @@ -178,7 +178,7 @@ def test_direction_vector_frame_axis(): direction_frame_axis='X', aberration_correction='S', anti_vector_flag=True, - ).payload == { + ) == { 'directionType': 'VECTOR', 'observer': 'EARTH', 'directionVectorType': 'REFERENCE_FRAME_AXIS', @@ -198,7 +198,7 @@ def test_direction_vector_in_instrument_fov_xyz(): direction_vector_x=0, direction_vector_y=0, direction_vector_z=1, - ).payload == { + ) == { 'directionType': 'VECTOR', 'directionVectorType': 'VECTOR_IN_INSTRUMENT_FOV', 'directionInstrument': 'CASSINI_ISS_NAC', @@ -218,7 +218,7 @@ def test_direction_vector_in_reference_frame_radec(): direction_frame='J2000', direction_vector_ra=0, direction_vector_dec=45, - ).payload == { + ) == { 'directionType': 'VECTOR', 'directionVectorType': 'VECTOR_IN_REFERENCE_FRAME', 'directionFrame': 'J2000', @@ -239,7 +239,7 @@ def test_direction_vector_in_reference_frame_azel(): direction_vector_el=15, azccw_flag=True, elplsz_flag='True', - ).payload == { + ) == { 'directionType': 'VECTOR', 'directionVectorType': 'VECTOR_IN_REFERENCE_FRAME', 'directionFrame': 'DSS-13_TOPO', @@ -260,7 +260,7 @@ def test_direction_vector_instrument_fov_boundary_vectors(): direction_vector_type='INSTRUMENT_FOV_BOUNDARY_VECTORS', direction_instrument='CASSINI_ISS_NAC', aberration_correction='CN', - ).payload == { + ) == { 'directionType': 'VECTOR', 'observer': 'CASSINI', 'directionVectorType': 'INSTRUMENT_FOV_BOUNDARY_VECTORS', diff --git a/tests/test_payload.py b/tests/test_payload.py index d997888..a9eb9c9 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -46,6 +46,9 @@ def baz(self, val): assert d.params == {'foo': 'bar', 'baz': 'X', 'qux': 'quux'} assert d.payload == {'foo': 'bar', 'baz': 'X'} # parameters only + # __eq__ + assert d == {'foo': 'bar', 'baz': 'X'} # compare with payload directly + assert repr(d) == '\n - foo: bar\n - baz: X' # __iter__ @@ -55,7 +58,7 @@ def baz(self, val): break # Only with required parameter(s) - assert DerivedPayload(foo='bar').payload == {'foo': 'bar'} + assert DerivedPayload(foo='bar') == {'foo': 'bar'} # Without required parameter(s) with raises(CalculationRequiredAttr): diff --git a/webgeocalc/payload.py b/webgeocalc/payload.py index 2dcf65f..bccc86c 100644 --- a/webgeocalc/payload.py +++ b/webgeocalc/payload.py @@ -44,6 +44,9 @@ def __iter__(self): if k.startswith('_') ) + def __eq__(self, other): + return self.payload == other + def _required(self, *attrs): """Check if the required arguments are in the params.""" for attr in attrs: From 382bc02a3f81c8e4017069610bea91747b635ab4 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 19:51:43 +0100 Subject: [PATCH 06/12] Simplify and clean up payload tests --- tests/test_angular_separation.py | 252 +++++++++----------------- tests/test_angular_size.py | 77 ++------ tests/test_frame_transformation.py | 4 +- tests/test_gf_coordinate_search.py | 6 +- tests/test_illumination_angles.py | 2 +- tests/test_osculating_elements.py | 73 ++------ tests/test_state_vector.py | 99 ++-------- tests/test_sub_observer_point.py | 87 ++------- tests/test_sub_solar_point.py | 2 +- tests/test_surface_intercept_point.py | 8 +- tests/test_time_conversion.py | 136 +++----------- 11 files changed, 185 insertions(+), 561 deletions(-) diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 87c6eb6..1cbe630 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -1,204 +1,122 @@ """Test WGC angular separation calculation.""" -from pytest import fixture, raises -from pytest import mark +from pytest import raises from webgeocalc import AngularSeparation from webgeocalc.direction import Direction from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr -@fixture -def kernel_paths(): - """Input kernel paths.""" - return [ - 'pds/wgc/kernels/lsk/naif0012.tls', - 'pds/wgc/kernels/spk/de430.bsp', - ] - - -@fixture -def time(): - """Input time.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def target_1(): - """Input name for the first target.""" - return 'VENUS' - - -@fixture -def target_2(): - """Input name for the second target.""" - return 'MERCURY' - - -@fixture -def observer(): - """Input name of the observer.""" - return 'SUN' - - -@fixture -def corr(): - """Input aberration correction.""" - return 'NONE' - - -@fixture -def params_two_targets(kernel_paths, time, target_1, target_2, observer, corr): - """Input parameters from WGC API example (TWO_TARGETS mode).""" - return { - 'kernel_paths': kernel_paths, - 'times': time, - 'target_1': target_1, - 'target_2': target_2, - 'observer': observer, - 'aberration_correction': corr, - } - - -@fixture -def payload_two_targets(kernel_paths, time, target_1, target_2, observer, corr): - """Payload from WGC API example (TWO_TARGETS mode).""" - return { +def test_angular_separation_two_targets_payload(): + """Test angular separation payload for TWO_TARGETS.""" + assert AngularSeparation( + kernel_paths=[ + 'pds/wgc/kernels/lsk/naif0012.tls', + 'pds/wgc/kernels/spk/de430.bsp', + ], + times='2012-10-19T08:24:00.000', + # spec_type='TWO_TARGETS', # Implicit / default + target_1='VENUS', + target_2='MERCURY', + observer='SUN', + aberration_correction='NONE', + ) == { "kernels": [{ "type": "KERNEL", - "path": kernel_paths[0], + "path": 'pds/wgc/kernels/lsk/naif0012.tls', }, { "type": "KERNEL", - "path": kernel_paths[1], + "path": 'pds/wgc/kernels/spk/de430.bsp', }], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [time], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "ANGULAR_SEPARATION", - "target1": target_1, + "target1": 'VENUS', "shape1": "POINT", - "target2": target_2, + "target2": 'MERCURY', "shape2": "POINT", - "observer": observer, - "aberrationCorrection": corr + "observer": 'SUN', + "aberrationCorrection": 'NONE', } -@fixture -def kernel_set(): - """Input kernel set.""" - return 5 - - -@fixture -def direction_position(): - """Input position direction as dict object.""" - return { - "direction_type": "POSITION", - "target": "SUN", - "shape": "POINT", - "observer": "CASSINI" - } - - -@fixture -def direction_position_payload(): - """Position direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "POSITION", - "target": "SUN", - "shape": "POINT", - "observer": "CASSINI" - } - - -@fixture -def direction_vector(): - """Input vector direction as Direction object.""" - return Direction( - direction_type='VECTOR', - direction_vector_type='REFERENCE_FRAME_AXIS', - direction_frame='CASSINI_RPWS_EDIPOLE', - direction_frame_axis='Z', - ) - - -@fixture -def direction_vector_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "REFERENCE_FRAME_AXIS", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionFrameAxis": "Z" - } - - -@fixture -def params_two_directions( - kernel_set, time, direction_vector, - direction_position, -): - """Input parameters for TWO_DIRECTIONS mode.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_vector, - 'direction_2': direction_position, - } - - -@fixture -def payload_two_directions( - kernel_set, time, direction_vector_payload, - direction_position_payload, -): - """Input parameters for TWO_DIRECTIONS mode.""" - return { +def test_angular_separation_two_directions_payload(): + """Test angular separation payload for TWO_DIRECTIONS.""" + assert AngularSeparation( + kernels=5, + times='2012-10-19T08:24:00.000', + spec_type='TWO_DIRECTIONS', + direction_1={ + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + }, + direction_2=Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='CASSINI_RPWS_EDIPOLE', + direction_frame_axis='Z', + ), + ) == { "kernels": [{ "type": "KERNEL_SET", - "id": kernel_set, + "id": 5, }], "specType": "TWO_DIRECTIONS", "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [time], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_payload, - "direction2": direction_position_payload, + "direction1": { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI", + }, + "direction2": { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "REFERENCE_FRAME_AXIS", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionFrameAxis": "Z", + }, } -@mark.parametrize('spec_type', [ - ('TWO_TARGETS'), - ('TWO_DIRECTIONS'), -]) -def test_angular_separation_payload(request, spec_type): - """Test angular separation payload.""" - params = request.getfixturevalue(f'params_{spec_type.lower()}') - payload = request.getfixturevalue(f'payload_{spec_type.lower()}') - - assert AngularSeparation(**params).payload == payload - - -def test_angular_separation_errors(params_two_targets, params_two_directions, - kernel_set, time): +def test_angular_separation_errors(): """Test angular separation payload errors.""" - # Missing observer for TWO_TARGETS - params_two_targets.pop('observer') + # Missing `observer` or `target_1/2` for TWO_TARGETS with raises(CalculationRequiredAttr): - AngularSeparation(**params_two_targets) - - # Missing direction_1 for TWO_DIRECTIONS - params_two_directions.pop('direction_1') + AngularSeparation( + kernels=5, + times='2012-10-19T08:24:00.000', + target_1='VENUS', + target_2='MERCURY', + ) + + # Missing `direction_1/2` for TWO_DIRECTIONS with raises(CalculationRequiredAttr): - AngularSeparation(**params_two_directions) + AngularSeparation( + kernels=5, + times='2012-10-19T08:24:00.000', + spec_type='TWO_DIRECTIONS', + direction_1={ + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + }, + ) # Invalid spec_type value with raises(CalculationInvalidAttr): - AngularSeparation(kernels=kernel_set, times=time, spec_type='WRONG') + AngularSeparation( + kernels=5, + times='2012-10-19T08:24:00.000', + spec_type='WRONG' + ) diff --git a/tests/test_angular_size.py b/tests/test_angular_size.py index 4afb73c..966abed 100644 --- a/tests/test_angular_size.py +++ b/tests/test_angular_size.py @@ -1,72 +1,23 @@ """Test WGC angular size calculation.""" -from pytest import fixture - from webgeocalc import AngularSize -@fixture -def kernels(): - """Input kernel.""" - return 5 # Cassini-Huygens - - -@fixture -def time(): - """Input time.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def target(): - """Input target name.""" - return 'ENCELADUS' - - -@fixture -def observer(): - """Input observer name.""" - return 'CASSINI' - - -@fixture -def corr(): - """Input aberration correction.""" - return 'CN+S' - - -@fixture -def params(kernels, time, target, observer, corr): - """Input parameters from WGC API example.""" - return { - 'kernels': kernels, - 'times': time, - 'target': target, - 'observer': observer, - 'aberration_correction': corr, - } - - -@fixture -def payload(kernels, time, target, observer, corr): - """Payload from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernels, - }], +def test_angular_size_payload(): + """Test angular size payload.""" + assert AngularSize( + kernels=5, # Cassini-Huygens + times='2012-10-19T08:24:00.000', + target='ENCELADUS', + observer='CASSINI', + aberration_correction='CN+S', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time, - ], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "ANGULAR_SIZE", - "target": target, - "observer": observer, - "aberrationCorrection": corr + "target": 'ENCELADUS', + "observer": 'CASSINI', + "aberrationCorrection": 'CN+S', } - - -def test_angular_size_payload(params, payload): - """Test angular size payload.""" - assert AngularSize(**params).payload == payload diff --git a/tests/test_frame_transformation.py b/tests/test_frame_transformation.py index ed90497..6d2eeb0 100644 --- a/tests/test_frame_transformation.py +++ b/tests/test_frame_transformation.py @@ -73,13 +73,13 @@ def payload(kernels, time, frame_1, frame_2, corr): "axis3": "Z", "angularUnits": "deg", "angularVelocityRepresentation": "VECTOR_IN_FRAME1", - "angularVelocityUnits": "deg/s" + "angularVelocityUnits": "deg/s", } def test_frame_transformation_payload(params, payload): """Test angular frame transformation payload.""" - assert FrameTransformation(**params).payload == payload + assert FrameTransformation(**params) == payload def test_frame_transformation_attr_err(params): diff --git a/tests/test_gf_coordinate_search.py b/tests/test_gf_coordinate_search.py index 4e718a5..2735e00 100644 --- a/tests/test_gf_coordinate_search.py +++ b/tests/test_gf_coordinate_search.py @@ -285,17 +285,17 @@ def payload_3(kernels, intervals, target, reference_frame, observer, time_step, def test_coordinate_search_default(params_1, payload_1): """Test Coordinate Search with single interval and default parameters.""" - assert GFCoordinateSearch(**params_1).payload == payload_1 + assert GFCoordinateSearch(**params_1) == payload_1 def test_coordinate_search_adjust_interval(params_2, payload_2): """Test Coordinate Search with single interval with adjusted interval option.""" - assert GFCoordinateSearch(**params_2).payload == payload_2 + assert GFCoordinateSearch(**params_2) == payload_2 def test_coordinate_search_filter_interval(params_3, payload_3): """Test Coordinate Search with single interval with filtered interval option.""" - assert GFCoordinateSearch(**params_3).payload == payload_3 + assert GFCoordinateSearch(**params_3) == payload_3 def test_coordinate_search_attr_type_err(params_1): diff --git a/tests/test_illumination_angles.py b/tests/test_illumination_angles.py index 7c68010..8cc95eb 100644 --- a/tests/test_illumination_angles.py +++ b/tests/test_illumination_angles.py @@ -96,7 +96,7 @@ def payload(kernels, time, target, target_frame, observer, lat, lon, corr): def test_illumination_angles_payload(params, payload): """Test illumination angles payload.""" - assert IlluminationAngles(**params).payload == payload + assert IlluminationAngles(**params) == payload def test_illumination_angles_attr_error(params): diff --git a/tests/test_osculating_elements.py b/tests/test_osculating_elements.py index 9115dad..e24a954 100644 --- a/tests/test_osculating_elements.py +++ b/tests/test_osculating_elements.py @@ -1,68 +1,25 @@ """Test WGC angular size calculation.""" -from pytest import fixture - from webgeocalc import OsculatingElements -@fixture -def kernels(): - """Kernels sets Solar and Cassini.""" - return [1, 5] - - -@fixture -def time(): - """Input time.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def orbiting(): - """Orbiting body.""" - return 'CASSINI' - - -@fixture -def center(): - """Center body.""" - return 'SATURN' - - -@fixture -def params(kernels, time, orbiting, center): - """Input parameters from WGC API example.""" - return { - 'kernels': kernels, - 'times': time, - 'orbiting_body': orbiting, - 'center_body': center, - } - - -@fixture -def payload(kernels, time, orbiting, center): - """Payload from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernels[0], - }, { - "type": "KERNEL_SET", - "id": kernels[1], - }], +def test_osculating_elements_payload(): + """Test osculating elements payload.""" + assert OsculatingElements( + kernels=[1, 5], + times='2012-10-19T08:24:00.000', + orbiting_body='CASSINI', + center_body='SATURN', + ) == { + "kernels": [ + {"type": "KERNEL_SET", "id": 1}, + {"type": "KERNEL_SET", "id": 5}, + ], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time, - ], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "OSCULATING_ELEMENTS", - "orbitingBody": orbiting, - "centerBody": center, + "orbitingBody": 'CASSINI', + "centerBody": 'SATURN', "referenceFrame": "J2000", } - - -def test_osculating_elements_payload(params, payload): - """Test osculating elements payload.""" - assert OsculatingElements(**params).payload == payload diff --git a/tests/test_state_vector.py b/tests/test_state_vector.py index 45b4ed1..dc773f7 100644 --- a/tests/test_state_vector.py +++ b/tests/test_state_vector.py @@ -1,90 +1,27 @@ """Test WGC state vector calculation.""" -from pytest import fixture - from webgeocalc import StateVector -@fixture -def kernels(): - """Cassini kernel set.""" - return 5 - - -@fixture -def target(): - """Input target.""" - return 'CASSINI' - - -@fixture -def observer(): - """Input observer.""" - return 'SATURN' - - -@fixture -def frame(): - """Input frame.""" - return 'IAU_SATURN' - - -@fixture -def time(): - """Input time.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def corr(): - """Input aberration correction.""" - return 'NONE' - - -@fixture -def state(): - """Input state.""" - return 'PLANETOGRAPHIC' - - -@fixture -def params(kernels, target, observer, frame, time, corr, state): - """Input parameters from WGC API example.""" - return { - 'kernels': kernels, - 'target': target, - 'observer': observer, - 'reference_frame': frame, - 'times': time, - 'aberration_correction': corr, - 'state_representation': state, - } - - -@fixture -def payload(kernels, target, observer, frame, time, corr, state): - """Payload from WGC API example.""" - return { - "kernels": [ - { - "type": "KERNEL_SET", - "id": kernels - } - ], +def test_state_vector_payload(): + """Test state vector payload.""" + assert StateVector( + kernels=5, + target='CASSINI', + observer='SATURN', + reference_frame='IAU_SATURN', + times='2012-10-19T08:24:00.000', + aberration_correction='NONE', + state_representation='PLANETOGRAPHIC', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time - ], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "STATE_VECTOR", - "target": target, - "observer": observer, - "referenceFrame": frame, - "aberrationCorrection": corr, - "stateRepresentation": state + "target": 'CASSINI', + "observer": 'SATURN', + "referenceFrame": 'IAU_SATURN', + "aberrationCorrection": 'NONE', + "stateRepresentation": 'PLANETOGRAPHIC', } - - -def test_state_vector_payload(params, payload): - """Test state vector payload.""" - assert StateVector(**params).payload == payload diff --git a/tests/test_sub_observer_point.py b/tests/test_sub_observer_point.py index c2c216b..a7e29d6 100644 --- a/tests/test_sub_observer_point.py +++ b/tests/test_sub_observer_point.py @@ -1,82 +1,27 @@ """Test WGC sub observer point calculation.""" -from pytest import fixture - from webgeocalc import SubObserverPoint -@fixture -def kernels(): - """Cassini kernel set.""" - return 5 - - -@fixture -def time(): - """Input time.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def target(): - """Input target.""" - return 'ENCELADUS' - - -@fixture -def target_frame(): - """Input target frame.""" - return 'IAU_ENCELADUS' - - -@fixture -def observer(): - """Input observer.""" - return 'CASSINI' - - -@fixture -def corr(): - """Input aberration correction.""" - return 'CN+S' - - -@fixture -def params(kernels, time, target, target_frame, observer, corr): - """Input parameters from WGC API example.""" - return { - 'kernels': kernels, - 'times': time, - 'target': target, - 'target_frame': target_frame, - 'observer': observer, - 'aberration_correction': corr, - } - - -@fixture -def payload(kernels, time, target, target_frame, observer, corr): - """Payload from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernels, - }], +def test_sub_observer_point_payload(): + """Test sub observer point payload.""" + assert SubObserverPoint( + kernels=5, + times='2012-10-19T08:24:00.000', + target='ENCELADUS', + target_frame='IAU_ENCELADUS', + observer='CASSINI', + aberration_correction='CN+S', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time, - ], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "SUB_OBSERVER_POINT", - "target": target, - "targetFrame": target_frame, - "observer": observer, + "target": 'ENCELADUS', + "targetFrame": 'IAU_ENCELADUS', + "observer": 'CASSINI', "subPointType": "Near point: ellipsoid", - "aberrationCorrection": corr, + "aberrationCorrection": 'CN+S', "stateRepresentation": "RECTANGULAR", } - - -def test_sub_observer_point_payload(params, payload): - """Test sub observer point payload.""" - assert SubObserverPoint(**params).payload == payload diff --git a/tests/test_sub_solar_point.py b/tests/test_sub_solar_point.py index 9d34015..b498d64 100644 --- a/tests/test_sub_solar_point.py +++ b/tests/test_sub_solar_point.py @@ -80,7 +80,7 @@ def payload(kernels, time, target, target_frame, observer, corr): def test_sub_solar_point_payload(params, payload): """Test sub observer point payload.""" - assert SubSolarPoint(**params).payload == payload + assert SubSolarPoint(**params) == payload def test_sub_solar_point_attr_error(params): diff --git a/tests/test_surface_intercept_point.py b/tests/test_surface_intercept_point.py index e572b1e..acb90e8 100644 --- a/tests/test_surface_intercept_point.py +++ b/tests/test_surface_intercept_point.py @@ -273,22 +273,22 @@ def payload_4(kernels, time, target, target_frame, observer, direction_vector_ty def test_surface_direction_point_payload_boresight(params_1, payload_1): """Test surface direction point payload with INSTRUMENT_BORESIGHT.""" - assert SurfaceInterceptPoint(**params_1).payload == payload_1 + assert SurfaceInterceptPoint(**params_1) == payload_1 def test_surface_direction_point_payload_reference_frame_axis(params_2, payload_2): """Test surface direction point payload with REFERENCE_FRAME_AXIS.""" - assert SurfaceInterceptPoint(**params_2).payload == payload_2 + assert SurfaceInterceptPoint(**params_2) == payload_2 def test_surface_direction_point_payload_vector_in_instrument_fov(params_3, payload_3): """Test surface direction point payload with VECTOR_IN_INSTRUMENT_FOV.""" - assert SurfaceInterceptPoint(**params_3).payload == payload_3 + assert SurfaceInterceptPoint(**params_3) == payload_3 def test_surface_direction_point_payload_vector_in_reference_frame(params_4, payload_4): """Test surface direction point payload with VECTOR_IN_REFERENCE_FRAME.""" - assert SurfaceInterceptPoint(**params_4).payload == payload_4 + assert SurfaceInterceptPoint(**params_4) == payload_4 def test_surface_direction_point_attr_error(params_1): diff --git a/tests/test_time_conversion.py b/tests/test_time_conversion.py index 8279c69..37ff1d1 100644 --- a/tests/test_time_conversion.py +++ b/tests/test_time_conversion.py @@ -1,126 +1,42 @@ """Test WGC time conversion calculation.""" -from pytest import fixture - from webgeocalc import TimeConversion -@fixture -def kernels(): - """Cassini kernel set.""" - return 5 - - -@fixture -def time_1(): - """Input time in spacecraft clock format.""" - return '1/1729329441.042' - - -@fixture -def time_2(): - """Input time 2.""" - return '2012-10-19T08:24:00.000' - - -@fixture -def time_system(): - """Input time system.""" - return 'SPACECRAFT_CLOCK' - - -@fixture -def time_format(): - """Input time format.""" - return 'SPACECRAFT_CLOCK_STRING' - - -@fixture -def sclk_id(): - """Cassini spacecraft id.""" - return -82 - - -@fixture -def output_time_format(): - """Input output time format.""" - return 'CUSTOM' - - -@fixture -def output_time_custom_format(): - """Input output time custom format.""" - return 'YYYY Month DD HR:MN' - - -@fixture -def params_1(kernels, time_1, time_system, time_format, sclk_id): - """Input parameters from WGC API example 1.""" - return { - 'kernels': kernels, - 'times': time_1, - 'time_system': time_system, - 'time_format': time_format, - 'sclk_id': sclk_id, - } - - -@fixture -def payload_1(kernels, time_1, time_system, time_format, sclk_id): - """Payload from WGC API example 1.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernels, - }], - "times": [ - time_1, - ], +def test_time_conversion_payload(): + """Test time conversion payload with spacecraft clock input.""" + assert TimeConversion( + kernels=5, + times='1/1729329441.042', + time_system='SPACECRAFT_CLOCK', + time_format='SPACECRAFT_CLOCK_STRING', + sclk_id=-82, + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "times": ['1/1729329441.042'], "calculationType": "TIME_CONVERSION", - "timeSystem": time_system, - "timeFormat": time_format, - "sclkId": sclk_id, + "timeSystem": 'SPACECRAFT_CLOCK', + "timeFormat": 'SPACECRAFT_CLOCK_STRING', + "sclkId": -82, "outputTimeSystem": "UTC", "outputTimeFormat": "CALENDAR", } -@fixture -def params_2(kernels, time_2, output_time_format, output_time_custom_format): - """Input parameters from WGC API example 2.""" - return { - 'kernels': kernels, - 'times': time_2, - 'output_time_format': output_time_format, - 'output_time_custom_format': output_time_custom_format, - } - - -@fixture -def payload_2(kernels, time_2, output_time_format, output_time_custom_format): - """Payload from WGC API example 2.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernels, - }], - "times": [ - time_2, - ], +def test_time_conversion_custom_format_payload(): + """Test time conversion payload with custom format output.""" + assert TimeConversion( + kernels=5, + times='2012-10-19T08:24:00.000', + output_time_format='CUSTOM', + output_time_custom_format='YYYY Month DD HR:MN', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "times": ['2012-10-19T08:24:00.000'], "calculationType": "TIME_CONVERSION", "timeSystem": "UTC", "timeFormat": "CALENDAR", "outputTimeSystem": "UTC", - "outputTimeFormat": output_time_format, - "outputTimeCustomFormat": output_time_custom_format, + "outputTimeFormat": 'CUSTOM', + "outputTimeCustomFormat": 'YYYY Month DD HR:MN', } - - -def test_time_conversion_payload(params_1, payload_1): - """Test time conversion payload with spacecraft clock input.""" - assert TimeConversion(**params_1).payload == payload_1 - - -def test_time_conversion_custom_format_payload(params_2, payload_2): - """Test time conversion payload with custom format output.""" - assert TimeConversion(**params_2).payload == payload_2 From 076aeedabf3aa0ef98389e8a2471376a135efdd8 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Mon, 24 Mar 2025 13:01:41 +0100 Subject: [PATCH 07/12] Add PhaseAngle endpoint --- docs/calculation.rst | 45 ++++++++++++++++++++- examples/calculation.ipynb | 69 +++++++++++++++++++++++++++------ tests/test_cli.py | 21 +++++++++- tests/test_phase_angle.py | 24 ++++++++++++ webgeocalc/__init__.py | 4 +- webgeocalc/calculation.py | 15 +++++++ webgeocalc/calculation_types.py | 59 ++++++++++++++++++++++++++++ webgeocalc/cli.py | 7 +++- webgeocalc/vars.py | 22 +++++------ 9 files changed, 237 insertions(+), 29 deletions(-) create mode 100644 tests/test_phase_angle.py diff --git a/docs/calculation.rst b/docs/calculation.rst index e358f49..0573f1d 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -3,19 +3,20 @@ WebGeoCalc calculations .. currentmodule:: webgeocalc -For now only the geometry/time calculations are implemented: +For now only these geometry/time calculations are implemented: - :py:class:`StateVector` - :py:class:`AngularSeparation` - :py:class:`AngularSize` - :py:class:`FrameTransformation` - :py:class:`IlluminationAngles` +- :py:class:`PhaseAngle` - :py:class:`SubSolarPoint` - :py:class:`SubObserverPoint` - :py:class:`SurfaceInterceptPoint` - :py:class:`OsculatingElements` -- :py:class:`TimeConversion` - :py:class:`GFCoordinateSearch` +- :py:class:`TimeConversion` Import generic WebGeoCalc calculation object: @@ -568,6 +569,46 @@ target as seen from an observer. .. autoclass:: IlluminationAngles +Phase Angle +----------- + +Calculate the phase angle defined by the centers of an illumination source, +a target and an observer. + +The phase angle is computed using the location of the bodies (if point objects) +or the center of the bodies (if finite bodies). The range of the phase angle is [0, pi]. + +.. testsetup:: + + from webgeocalc import PhaseAngle + +>>> PhaseAngle( +... kernels = 5, +... times = '2012-10-19T08:24:00.000', +... target = 'ENCELADUS', +... target_frame = 'IAU_ENCELADUS', +... observer = 'CASSINI', +... illuminator = 'SUN', +... aberration_correction = 'CN+S', +... verbose = False, +... ).run() +{'DATE': '2012-10-19 08:24:00.000000 UTC', 'PHASE_ANGLE': 0.99571442} + +.. important:: + + Calculation required parameters: + - :py:attr:`~Calculation.kernels` or/and :py:attr:`~Calculation.kernel_paths` + - :py:attr:`~Calculation.times` or :py:attr:`~Calculation.intervals` with :py:attr:`~Calculation.time_step` and :py:attr:`~Calculation.time_step_units` + - :py:attr:`~Calculation.target` + - :py:attr:`~Calculation.observer` + + Default parameters: + - :py:attr:`~Calculation.illuminator`: ``SUN`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + +.. autoclass:: PhaseAngle + + Sub Solar Point --------------- diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index 27187b8..8d843ab 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -516,6 +516,51 @@ ").run()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Phase Angle\n", + "\n", + "Calculate the phase angle defined by the centers of an illumination source, a target and an observer: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#phase-angle))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Calculation submit] Phase: COMPLETE (id: 8019634b-98c2-4216-8a2a-3294b00d2197)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'PHASE_ANGLE': 0.99571442}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from webgeocalc import PhaseAngle\n", + "\n", + "PhaseAngle(\n", + " kernels = 5,\n", + " times = '2012-10-19T08:24:00.000',\n", + " target = 'ENCELADUS',\n", + " observer = 'CASSINI',\n", + " illuminator = 'SUN',\n", + " aberration_correction = 'CN+S',\n", + ").run()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -527,7 +572,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { @@ -553,7 +598,7 @@ " 'LIGHT_TIME': 3.22771614}" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -582,7 +627,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -609,7 +654,7 @@ " 'LTST': '11:58:49'}" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -638,7 +683,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -664,7 +709,7 @@ " 'LTST': '20:03:06'}" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -697,7 +742,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -724,7 +769,7 @@ " 'CENTER_BODY_GM': 37931207.49865224}" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -751,7 +796,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -767,7 +812,7 @@ "{'DATE': '1/1729329441.004', 'DATE2': '2012-10-19 08:24:02.919085 UTC'}" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -795,7 +840,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -811,7 +856,7 @@ "{'DATE': '2012-10-19 08:39:33.814938 UTC', 'DURATION': 3394.11539114}" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } diff --git a/tests/test_cli.py b/tests/test_cli.py index 75060b8..bd2d076 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,7 @@ cli_bodies, cli_frame_transformation, cli_frames, cli_gf_coordinate_search, cli_illumination_angles, cli_instruments, cli_kernel_sets, - cli_osculating_elements, cli_state_vector, + cli_osculating_elements, cli_phase_angle, cli_state_vector, cli_subobserver_point, cli_subsolar_point, cli_surface_intercept_point, cli_time_conversion) @@ -368,6 +368,25 @@ def test_cli_illumination_angles_dry_run(capsys): assert "calculationType: ILLUMINATION_ANGLES," in captured.out +def test_cli_phase_angle_dry_run(capsys): + """Test dry-run phase angle calculation parameter with the CLI.""" + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00.000 ' + '--target ENCELADUS ' + '--observer CASSINI ' + '--aberration_correction CN+S ').split() + + cli_phase_angle(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: PHASE_ANGLE," in captured.out + assert "target: ENCELADUS" in captured.out + assert "observer: CASSINI" in captured.out + assert "illuminator: SUN" in captured.out + assert "aberrationCorrection: CN+S" in captured.out + + def test_cli_subsolar_point_dry_run(capsys): """Test dry-run sub-solar point calculation parameter with the CLI.""" argv = ('--dry-run ' diff --git a/tests/test_phase_angle.py b/tests/test_phase_angle.py new file mode 100644 index 0000000..0ad3ec3 --- /dev/null +++ b/tests/test_phase_angle.py @@ -0,0 +1,24 @@ +"""Test WGC phase angle calculation.""" + +from webgeocalc import PhaseAngle + + +def test_phase_angle_payload(): + """Test phase angle payload.""" + assert PhaseAngle( + kernels=5, + times='2012-10-19T08:24:00.000', + target='CASSINI', + observer='SATURN', + aberration_correction='CN+S', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": ['2012-10-19T08:24:00.000'], + "calculationType": "PHASE_ANGLE", + "target": 'CASSINI', + "observer": 'SATURN', + "illuminator": 'SUN', + "aberrationCorrection": 'CN+S', + } diff --git a/webgeocalc/__init__.py b/webgeocalc/__init__.py index d55aa5e..5b2c3b4 100644 --- a/webgeocalc/__init__.py +++ b/webgeocalc/__init__.py @@ -4,7 +4,7 @@ from .calculation import Calculation from .calculation_types import (AngularSeparation, AngularSize, FrameTransformation, GFCoordinateSearch, - IlluminationAngles, OsculatingElements, + IlluminationAngles, OsculatingElements, PhaseAngle, StateVector, SubObserverPoint, SubSolarPoint, SurfaceInterceptPoint, TimeConversion) from .version import __version__ @@ -21,7 +21,7 @@ 'AngularSize', 'FrameTransformation', 'IlluminationAngles', - # 'PhaseAngle', + 'PhaseAngle', # 'PointingDirection', 'SubSolarPoint', 'SubObserverPoint', diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 99cadbb..3529378 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -940,6 +940,21 @@ def observer(self, val): """ self.__observer = val if isinstance(val, int) else val.upper() + @parameter + def illuminator(self, val): + """The illumination source. + + Often, the illumination source is the Sun, + but it could be any other ephemeris object. + + Parameters + ---------- + illuminator: str or int + The observing body ``name`` or ``id`` from :py:func:`API.bodies`. + + """ + self.__illuminator = val if isinstance(val, int) else val.upper() + @parameter def reference_frame(self, val): """The reference frame. diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index 894a6ca..609e83f 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -390,6 +390,65 @@ def __init__(self, shape_1='ELLIPSOID', coordinate_representation='LATITUDINAL', super().__init__(**kwargs) +class PhaseAngle(Calculation): + """Phase angle calculation. + + Calculate the phase angle defined by the centers of an illumination source, + a target and an observer. + + The phase angle is computed using the location of the bodies (if point objects) + or the center of the bodies (if finite bodies). + + The range of the phase angle is [0, pi]. + + Parameters + ---------- + target: str or int + See: :py:attr:`target` + observer: str or int + See: :py:attr:`observer` + illuminator: str or int, optional + See: :py:attr:`illuminator` (default: ``SUN``) + aberration_correction: str, optional + See: :py:attr:`aberration_correction` (default: ``CN``) + + Other Parameters + ---------------- + kernels: str, int, [str or/and int] + See: :py:attr:`kernels` + kernel_paths: str, [str] + See: :py:attr:`kernel_paths` + times: str or [str] + See: :py:attr:`times` + intervals: [str, str] or {'startTime': str, 'endTime': str} or [interval, ...] + See: :py:attr:`intervals` + time_step: int + See: :py:attr:`time_step` + time_step_units: str + See: :py:attr:`time_step_units` + time_system: str + See: :py:attr:`time_system` + time_format: str + See: :py:attr:`time_format` + + Raises + ------ + CalculationRequiredAttr + If :py:attr:`target`, or :py:attr:`observer` are not provided. + + """ + + REQUIRED = ('target', 'observer') + + def __init__(self, illuminator='SUN', aberration_correction='CN', **kwargs): + + kwargs['calculation_type'] = 'PHASE_ANGLE' + kwargs['illuminator'] = illuminator + kwargs['aberration_correction'] = aberration_correction + + super().__init__(**kwargs) + + class SubSolarPoint(Calculation): """Sub-solar point calculation. diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index a45f040..7509e00 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -6,7 +6,7 @@ from .api import Api, ESA_API, JPL_API from .calculation_types import (AngularSeparation, AngularSize, Calculation, FrameTransformation, GFCoordinateSearch, - IlluminationAngles, OsculatingElements, + IlluminationAngles, OsculatingElements, PhaseAngle, StateVector, SubObserverPoint, SubSolarPoint, SurfaceInterceptPoint, TimeConversion) from .errors import KernelSetNotFound, TooManyKernelSets @@ -325,6 +325,11 @@ def cli_illumination_angles(argv=None): cli_calculation(argv, IlluminationAngles, desc='Illumination Angles') +def cli_phase_angle(argv=None): + """Submit phase angle calculation with the CLI.""" + cli_calculation(argv, PhaseAngle, desc='Phase Angle') + + def cli_subsolar_point(argv=None): """Submit sub-solar point calculation with the CLI.""" cli_calculation(argv, SubSolarPoint, desc='Sub-Solar Point') diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 603c316..be407b8 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -17,7 +17,7 @@ 'ANGULAR_SIZE', 'FRAME_TRANSFORMATION', 'ILLUMINATION_ANGLES', - # 'PHASE_ANGLE', + 'PHASE_ANGLE', # 'POINTING_DIRECTION', 'SUB_SOLAR_POINT', 'SUB_OBSERVER_POINT', @@ -25,16 +25,16 @@ # 'TANGENT_POINT', 'OSCULATING_ELEMENTS', 'GF_COORDINATE_SEARCH', - 'GF_ANGULAR_SEPARATION_SEARCH', - 'GF_DISTANCE_SEARCH', - 'GF_SUB_POINT_SEARCH', - 'GF_OCCULTATION_SEARCH', - 'GF_SURFACE_INTERCEPT_POINT_SEARCH', - 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', - 'GF_RAY_IN_FOV_SEARCH', - 'GF_RANGE_RATE_SEARCH', - 'GF_PHASE_ANGLE_SEARCH', - 'GF_ILLUMINATION_ANGLES_SEARCH', + # 'GF_ANGULAR_SEPARATION_SEARCH', + # 'GF_DISTANCE_SEARCH', + # 'GF_SUB_POINT_SEARCH', + # 'GF_OCCULTATION_SEARCH', + # 'GF_SURFACE_INTERCEPT_POINT_SEARCH', + # 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', + # 'GF_RAY_IN_FOV_SEARCH', + # 'GF_RANGE_RATE_SEARCH', + # 'GF_PHASE_ANGLE_SEARCH', + # 'GF_ILLUMINATION_ANGLES_SEARCH', 'TIME_CONVERSION', ], 'TIME_SYSTEM': [ From f31cee2ce2d922e1ccdfbd4c0a1e78da3a8f935c Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Mon, 24 Mar 2025 16:59:09 +0100 Subject: [PATCH 08/12] Add PointingDirection endpoint --- docs/calculation.rst | 92 ++++++++++++++++++++++++++++++ docs/cli.rst | 5 +- examples/calculation.ipynb | 72 +++++++++++++++++++---- tests/test_cli.py | 37 +++++++++++- tests/test_pointing_direction.py | 84 +++++++++++++++++++++++++++ webgeocalc/__init__.py | 6 +- webgeocalc/calculation.py | 98 ++++++++++++++++++++++++++++++-- webgeocalc/calculation_types.py | 87 +++++++++++++++++++++++++++- webgeocalc/cli.py | 9 ++- webgeocalc/vars.py | 21 ++++++- 10 files changed, 483 insertions(+), 28 deletions(-) create mode 100644 tests/test_pointing_direction.py diff --git a/docs/calculation.rst b/docs/calculation.rst index 0573f1d..7d90b39 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -11,11 +11,23 @@ For now only these geometry/time calculations are implemented: - :py:class:`FrameTransformation` - :py:class:`IlluminationAngles` - :py:class:`PhaseAngle` +- :py:class:`PointingDirection` - :py:class:`SubSolarPoint` - :py:class:`SubObserverPoint` - :py:class:`SurfaceInterceptPoint` +- :py:class:`TangentPoint` - :py:class:`OsculatingElements` - :py:class:`GFCoordinateSearch` +- ``GFAngularSeparationSearch`` (not implemented) +- ``GFDistanceSearch`` (not implemented) +- ``GFSubPointSearch`` (not implemented) +- ``GFOccultationSearch`` (not implemented) +- ``GFSurfaceInterceptPointSearch`` (not implemented) +- ``GFTargetInInstrumentFovSearch`` (not implemented) +- ``GFRayInFovSearch`` (not implemented) +- ``GFRangeRateSearch`` (not implemented) +- ``GFPhaseAngleSearch`` (not implemented) +- ``GFIlluminationAnglesSearch`` (not implemented) - :py:class:`TimeConversion` Import generic WebGeoCalc calculation object: @@ -267,6 +279,12 @@ web portals. * - :py:class:`IlluminationAngles` - ``ILLUMINATION_ANGLES`` - Illumination Angles + * - :py:class:`PhaseAngle` + - ``PHASE_ANGLE`` + - Phase Angle + * - :py:class:`PointingDirection` + - ``POINTING_DIRECTION`` + - Pointing Direction * - :py:class:`SubSolarPoint` - ``SUB_SOLAR_POINT`` - Sub-solar Point @@ -609,6 +627,80 @@ or the center of the bodies (if finite bodies). The range of the phase angle is .. autoclass:: PhaseAngle +Pointing Direction +------------------ + +Calculates the pointing direction in a user specified reference frame and output +it as a unit or full magnitude vector represented in a user specified coordinate system. + +The direction can be specified by the position or velocity of an object with respect +to an observer, or by directly providing a vector, which could be specified as coordinate +in a given frame, a frame axis, an instrument boresight, or as instrument FoV corner vectors. + +The output reference frame may be different than the one used for specification +of the input vector (if such option is selected). If so, the orientations of both frames +relative to the inertial space are computed at the same time taking into account the +aberration corrections given in the direction specification. + +The output direction could be given as unit or non-unit vector in Rectangular, +Azimuth/Elevation, Right Ascension/Declination/Range, Planetocentric, Cylindrical, +or Spherical coordinates. + +.. testsetup:: + + from webgeocalc import PointingDirection + +>>> PointingDirection( +... kernels = 5, +... times = '2012-10-19T08:24:00.000', +... direction = { +... 'direction_type': 'VECTOR', +... 'observer': 'Cassini', +... 'direction_vector_type': 'INSTRUMENT_FOV_BOUNDARY_VECTORS', +... 'direction_instrument': 'CASSINI_ISS_NAC', +... 'aberration_correction': 'CN', +... }, +... reference_frame = 'J2000', +... coordinate_representation = 'RA_DEC', +... verbose = False, +... ).run() +{'DATE': ['2012-10-19 08:24:00.000000 UTC', + '2012-10-19 08:24:00.000000 UTC', + '2012-10-19 08:24:00.000000 UTC', + '2012-10-19 08:24:00.000000 UTC'], + 'BOUNDARY_POINT_NUMBER': [1.0, 2.0, 3.0, 4.0], + 'RIGHT_ASCENSION': [209.67659835, 209.39258312, 209.60578464, 209.88990474], + 'DECLINATION': [-9.57807208, -9.78811104, -10.06810257, -9.85788588], + 'RANGE': [1.0, 1.0, 1.0, 1.0]} + + +.. important:: + + Calculation required parameters: + - :py:attr:`~Calculation.kernels` or/and :py:attr:`~Calculation.kernel_paths` + - :py:attr:`~Calculation.times` or :py:attr:`~Calculation.intervals` with :py:attr:`~Calculation.time_step` and :py:attr:`~Calculation.time_step_units` + - :py:attr:`~Calculation.direction` + - :py:attr:`~Calculation.reference_frame` + + Default parameters: + - :py:attr:`~Calculation.vector_magnitude`: ``UNIT`` + - :py:attr:`~Calculation.coordinate_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + + Additional required parameters for ``coordinate_representation='AZ_EL'``: + - :py:attr:`~Calculation.azccw_flag` + - :py:attr:`~Calculation.elplsz_flag` + +.. hint:: + + The direction can be specified either with an explicit :py:type:`dict` + or with a :py:class:`Direction` object (see :py:class:`AngularSeparation` + with ``TWO_DIRECTIONS``). + +.. autoclass:: PointingDirection + + Sub Solar Point --------------- diff --git a/docs/cli.rst b/docs/cli.rst index d63ab3c..74f0ce4 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -243,6 +243,8 @@ Here is the list of all the calculation entry points available on the CLI: - ``wgc-angular-size`` - ``wgc-frame-transformation`` - ``wgc-illumination-angles`` +- ``wgc-phase-angle`` +- ``wgc-pointing_direction`` - ``wgc-subsolar-point`` - ``wgc-subobserver-point`` - ``wgc-surface-intercept-point`` @@ -253,7 +255,8 @@ Here is the list of all the calculation entry points available on the CLI: .. hint:: If you need to provide a :py:class:`~webgeocalc.direction.Direction` - (eg. an :py:class:`~webgeocalc.AngularSeparation` calculation with ``TWO_DIRECTIONS``). + (eg. an :py:class:`~webgeocalc.AngularSeparation` calculation with ``TWO_DIRECTIONS`` or + for :py:class:`~webgeocalc.PointingDirection`). You need to encapsulate the nested parameters into single (``'``) or double (``"``) quotes separated with spaces: diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index 8d843ab..66a821c 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -561,6 +561,54 @@ ").run()" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Calculation update] Phase: COMPLETE (id: e966681b-5433-4814-b494-e42a6b2a1391)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'DATE': ['2012-10-19 08:24:00.000000 UTC',\n", + " '2012-10-19 08:24:00.000000 UTC',\n", + " '2012-10-19 08:24:00.000000 UTC',\n", + " '2012-10-19 08:24:00.000000 UTC'],\n", + " 'BOUNDARY_POINT_NUMBER': [1.0, 2.0, 3.0, 4.0],\n", + " 'RIGHT_ASCENSION': [209.67659835, 209.39258312, 209.60578464, 209.88990474],\n", + " 'DECLINATION': [-9.57807208, -9.78811104, -10.06810257, -9.85788588],\n", + " 'RANGE': [1.0, 1.0, 1.0, 1.0]}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from webgeocalc import PointingDirection\n", + "\n", + "PointingDirection(\n", + " kernels = 5,\n", + " times = '2012-10-19T08:24:00.000',\n", + " direction = {\n", + " 'direction_type': 'VECTOR',\n", + " 'observer': 'Cassini',\n", + " 'direction_vector_type': 'INSTRUMENT_FOV_BOUNDARY_VECTORS',\n", + " 'direction_instrument': 'CASSINI_ISS_NAC',\n", + " 'aberration_correction': 'CN',\n", + " },\n", + " reference_frame='J2000',\n", + " coordinate_representation='RA_DEC',\n", + ").run()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -572,7 +620,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { @@ -598,7 +646,7 @@ " 'LIGHT_TIME': 3.22771614}" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -627,7 +675,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -654,7 +702,7 @@ " 'LTST': '11:58:49'}" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -683,7 +731,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -709,7 +757,7 @@ " 'LTST': '20:03:06'}" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -742,7 +790,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -769,7 +817,7 @@ " 'CENTER_BODY_GM': 37931207.49865224}" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -796,7 +844,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -812,7 +860,7 @@ "{'DATE': '1/1729329441.004', 'DATE2': '2012-10-19 08:24:02.919085 UTC'}" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -840,7 +888,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -856,7 +904,7 @@ "{'DATE': '2012-10-19 08:39:33.814938 UTC', 'DURATION': 3394.11539114}" ] }, - "execution_count": 17, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } diff --git a/tests/test_cli.py b/tests/test_cli.py index bd2d076..33a6920 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,7 +4,8 @@ cli_bodies, cli_frame_transformation, cli_frames, cli_gf_coordinate_search, cli_illumination_angles, cli_instruments, cli_kernel_sets, - cli_osculating_elements, cli_phase_angle, cli_state_vector, + cli_osculating_elements, cli_phase_angle, + cli_pointing_direction, cli_state_vector, cli_subobserver_point, cli_subsolar_point, cli_surface_intercept_point, cli_time_conversion) @@ -387,6 +388,40 @@ def test_cli_phase_angle_dry_run(capsys): assert "aberrationCorrection: CN+S" in captured.out +def test_cli_pointing_direction_dry_run(capsys): + """Test dry-run pointing direction calculation parameter with the CLI.""" + argv = [ + '--dry-run', + '--kernels', '5', + '--times', '2012-10-19T08:24:00', + '--direction', "'direction_type=VECTOR " + "observer=CASSINI " + "direction_vector_type=INSTRUMENT_FOV_BOUNDARY_VECTORS " + "direction_instrument=CASSINI_ISS_NAC " + "aberration_correction=CN'", + '--reference_frame', 'J2000', + '--coordinate_representation', 'RA_DEC', + ] + + cli_pointing_direction(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: POINTING_DIRECTION," in captured.out + assert ( + "direction: {" + "'directionType': 'VECTOR', " + "'observer': 'CASSINI', " + "'directionVectorType': 'INSTRUMENT_FOV_BOUNDARY_VECTORS', " + "'directionInstrument': 'CASSINI_ISS_NAC', " + "'aberrationCorrection': 'CN', " + "'antiVectorFlag': False" + "}" + ) in captured.out + assert "referenceFrame: J2000" in captured.out + assert "coordinateRepresentation: RA_DEC" in captured.out + assert "vectorMagnitude: UNIT" in captured.out + + def test_cli_subsolar_point_dry_run(capsys): """Test dry-run sub-solar point calculation parameter with the CLI.""" argv = ('--dry-run ' diff --git a/tests/test_pointing_direction.py b/tests/test_pointing_direction.py new file mode 100644 index 0000000..c7ce731 --- /dev/null +++ b/tests/test_pointing_direction.py @@ -0,0 +1,84 @@ +"""Test WGC pointing direction calculation.""" + +from pytest import raises + +from webgeocalc import PointingDirection +from webgeocalc.direction import Direction +from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr + + +def test_pointing_direction_payload(): + """Test pointing direction payload.""" + assert PointingDirection( + kernels=5, + times='2012-10-19T08:24:00.000', + direction={ + 'direction_type': 'VECTOR', + 'observer': 'CASSINI', + 'direction_vector_type': 'INSTRUMENT_FOV_BOUNDARY_VECTORS', + 'direction_instrument': 'CASSINI_ISS_NAC', + 'aberration_correction': 'CN', + }, + reference_frame='DSS-13_TOPO', + coordinate_representation='AZ_EL', + azccw_flag='FALSE', + elplsz_flag='false', + ).payload == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": ['2012-10-19T08:24:00.000'], + "direction": { + "directionType": "VECTOR", + "observer": "CASSINI", + "directionVectorType": "INSTRUMENT_FOV_BOUNDARY_VECTORS", + "directionInstrument": "CASSINI_ISS_NAC", + "aberrationCorrection": "CN", + "antiVectorFlag": False, + }, + "referenceFrame": "DSS-13_TOPO", + "calculationType": "POINTING_DIRECTION", + "vectorMagnitude": "UNIT", + "coordinateRepresentation": "AZ_EL", + "azccwFlag": False, + "elplszFlag": False, + } + + +def test_pointing_direction_errors(): + """Test pointing direction errors.""" + direction = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + ) + + # Invalid `vector_magnitude` + with raises(CalculationInvalidAttr): + PointingDirection( + kernels=5, + times='2012-10-19T08:24:00.000', + direction=direction, + reference_frame='J2000', + vector_magnitude='WRONG', + ) + + # Invalid `coordinate_representation` (PLANETODETIC is excluded) + with raises(CalculationInvalidAttr): + PointingDirection( + kernels=5, + times='2012-10-19T08:24:00.000', + direction=direction, + reference_frame='IAU_EARTH', + coordinate_representation='PLANETODETIC', + ) + + # Missing `azccw_flag` or `elplsz_flag` with `coordinate_representation='AZ_EL'` + with raises(CalculationRequiredAttr): + PointingDirection( + kernels=5, + times='2012-10-19T08:24:00.000', + direction=direction, + reference_frame='DSS-13_TOPO', + coordinate_representation='AZ_EL', + ) diff --git a/webgeocalc/__init__.py b/webgeocalc/__init__.py index 5b2c3b4..96f6489 100644 --- a/webgeocalc/__init__.py +++ b/webgeocalc/__init__.py @@ -5,8 +5,8 @@ from .calculation_types import (AngularSeparation, AngularSize, FrameTransformation, GFCoordinateSearch, IlluminationAngles, OsculatingElements, PhaseAngle, - StateVector, SubObserverPoint, SubSolarPoint, - SurfaceInterceptPoint, TimeConversion) + PointingDirection, StateVector, SubObserverPoint, + SubSolarPoint, SurfaceInterceptPoint, TimeConversion) from .version import __version__ @@ -22,7 +22,7 @@ 'FrameTransformation', 'IlluminationAngles', 'PhaseAngle', - # 'PointingDirection', + 'PointingDirection', 'SubSolarPoint', 'SubObserverPoint', 'SurfaceInterceptPoint', diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 3529378..c3a3203 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -327,7 +327,7 @@ def results(self): return {column.outputID: value for column, value in zip(self.columns, data)} def run(self, timeout=30, sleep=1): - """Submit, update and retrive calculation results at once. + """Submit, update and retrieve calculation results at once. See: :py:func:`submit`, :py:func:`update` and :py:attr:`results`. @@ -1061,6 +1061,21 @@ def spec_type(self, val): """ self.__specType = val + @parameter + def direction(self, val): + """The direction specification object. + + Parameters + ---------- + direction: dict or Direction + Direction position/velocity/vector. See: :py:class:`Direction`. + + """ + if not isinstance(val, Direction): + val = Direction(**val) + + self.__direction = val.payload + @parameter def direction_1(self, val): """The first direction object. @@ -1071,7 +1086,7 @@ def direction_1(self, val): Parameters ---------- direction_1: dict or Direction - Direction vector. See: :py:class:`Direction`. + Direction position/velocity/vector. See: :py:class:`Direction`. """ if not isinstance(val, Direction): @@ -1089,7 +1104,7 @@ def direction_2(self, val): Parameters ---------- direction_2: dict or Direction - Direction vector. See: :py:class:`Direction`. + Direction position/velocity/vector. See: :py:class:`Direction`. """ if not isinstance(val, Direction): @@ -1097,6 +1112,22 @@ def direction_2(self, val): self.__direction2 = val.payload + @parameter(only='VECTOR_MAGNITUDE') + def vector_magnitude(self, val): + """Magnitude of the output vector representation. + + One of: + + - ``UNIT`` + - ``PRESERVE_ORIGINAL`` + + Use ``UNIT`` to represent the output direction vector + as a unit vector, and ``PRESERVE_ORIGINAL`` to output + the direction vector with its computed magnitude. + + """ + self.__vectorMagnitude = val + @parameter(only='STATE_REPRESENTATION') def state_representation(self, val): """State representation. @@ -1369,9 +1400,14 @@ def coordinate_representation(self, val): coordinate_representation: str One of: - - LATITUDINAL *(planetocentric)* - - PLANETODETIC - - PLANETOGRAPHIC + - ``RECTANGULAR`` + - ``RA_DEC`` + - ``LATITUDINAL`` *(planetocentric)* + - ``PLANETODETIC`` (not for ``POINTING_DIRECTION``) + - ``PLANETOGRAPHIC`` (not for ``POINTING_DIRECTION``) + - ``CYLINDRICAL`` + - ``SPHERICAL`` + - ``AZ_EL`` Raises ------ @@ -1421,6 +1457,56 @@ def longitude(self, val): else: raise CalculationInvalidValue('longitude', val, -180, 180) + @parameter(only='BOOLEAN') + def azccw_flag(self, val): + """Flag indicating how azimuth is measured. + + If ``azccw_flag`` is ``True``, azimuth increases in the counterclockwise + direction; otherwise it increases in the clockwise direction. + + Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL``. + + Parameters + ---------- + azccw_flag: bool or str + Azimuth orientation. + + Raises + ------ + CalculationInvalidValue + If ``azccw_flag`` not a boolean. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + self.__azccwFlag = val + + @parameter(only='BOOLEAN') + def elplsz_flag(self, val): + """Flag indicating how elevation is measured. + + If ``elplsz_flag`` is ``True``, elevation increases from the XY plane + toward +Z; otherwise toward -Z. + + Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL``. + + Parameters + ---------- + elplsz_flag: bool or str + Azimuth orientation. + + Raises + ------ + CalculationInvalidValue + If ``elplsz_flag`` not a boolean. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + self.__elplszFlag = val + @parameter(only='SUB_POINT_TYPE') def sub_point_type(self, val): """Sub-observer point. diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index 609e83f..f4ab0bb 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -434,7 +434,7 @@ class PhaseAngle(Calculation): Raises ------ CalculationRequiredAttr - If :py:attr:`target`, or :py:attr:`observer` are not provided. + If :py:attr:`target` or :py:attr:`observer` are not provided. """ @@ -449,6 +449,91 @@ def __init__(self, illuminator='SUN', aberration_correction='CN', **kwargs): super().__init__(**kwargs) +class PointingDirection(Calculation): + """Pointing direction calculation. + + Calculates the pointing direction in a user specified reference frame and + output it as a unit or full magnitude vector represented in a user + specified coordinate system. + + The direction can be specified by the position or velocity of an object + with respect to an observer, or by directly providing a vector, + which could be specified as coordinates in a given frame, a frame axis, + an instrument boresight, or as instrument FoV corner vectors. + + The output reference frame may be different than the one used for specification + of the input vector (if such option is selected). If so, the orientations + of both frames relative to the inertial space are computed at the same time + taking into account the aberration corrections given in the direction specification. + + The output direction could be given as unit or non-unit vector in + Rectangular, Azimuth/Elevation, Right Ascension/Declination/Range, + Planetocentric, Cylindrical, or Spherical coordinates. + + Parameters + ---------- + direction: dict or Direction + See: :py:attr:`direction` + reference_frame: str or int + See: :py:attr:`reference_frame` + vector_magnitude: str, optional + See: :py:attr:`vector_magnitude` (default: ``UNIT``) + coordinate_representation: str, optional + See: :py:attr:`coordinate_representation` (default: ``RECTANGULAR``) + + Other Parameters + ---------------- + kernels: str, int, [str or/and int] + See: :py:attr:`kernels` + kernel_paths: str, [str] + See: :py:attr:`kernel_paths` + times: str or [str] + See: :py:attr:`times` + intervals: [str, str] or {'startTime': str, 'endTime': str} or [interval, ...] + See: :py:attr:`intervals` + time_step: int + See: :py:attr:`time_step` + time_step_units: str + See: :py:attr:`time_step_units` + time_system: str + See: :py:attr:`time_system` + time_format: str + See: :py:attr:`time_format` + + Required parameters `AZ_EL` + + azccw_flag: bool or str + See: :py:attr:`azccw_flag` + elplsz_flag: bool or str + See: :py:attr:`elplsz_flag` + + Raises + ------ + CalculationRequiredAttr + If :py:attr:`target` or :py:attr:`reference_frame` are not provided. + + """ + + REQUIRED = ('direction', 'reference_frame') + + def __init__(self, vector_magnitude='UNIT', + coordinate_representation='RECTANGULAR', **kwargs): + + valid = VALID_PARAMETERS['COORDINATE_REPRESENTATION_POINTING_DIRECTION'] + if coordinate_representation not in valid: + raise CalculationInvalidAttr( + 'coordinate_representation', coordinate_representation, valid) + + if coordinate_representation == 'AZ_EL': + self.REQUIRED += ('azccw_flag', 'elplsz_flag') + + kwargs['calculation_type'] = 'POINTING_DIRECTION' + kwargs['vector_magnitude'] = vector_magnitude + kwargs['coordinate_representation'] = coordinate_representation + + super().__init__(**kwargs) + + class SubSolarPoint(Calculation): """Sub-solar point calculation. diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 7509e00..44af494 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -7,8 +7,8 @@ from .calculation_types import (AngularSeparation, AngularSize, Calculation, FrameTransformation, GFCoordinateSearch, IlluminationAngles, OsculatingElements, PhaseAngle, - StateVector, SubObserverPoint, SubSolarPoint, - SurfaceInterceptPoint, TimeConversion) + PointingDirection, StateVector, SubObserverPoint, + SubSolarPoint, SurfaceInterceptPoint, TimeConversion) from .errors import KernelSetNotFound, TooManyKernelSets @@ -330,6 +330,11 @@ def cli_phase_angle(argv=None): cli_calculation(argv, PhaseAngle, desc='Phase Angle') +def cli_pointing_direction(argv=None): + """Submit pointing direction calculation with the CLI.""" + cli_calculation(argv, PointingDirection, desc='Pointing Direction') + + def cli_subsolar_point(argv=None): """Submit sub-solar point calculation with the CLI.""" cli_calculation(argv, SubSolarPoint, desc='Sub-Solar Point') diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index be407b8..3387e08 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -18,7 +18,7 @@ 'FRAME_TRANSFORMATION', 'ILLUMINATION_ANGLES', 'PHASE_ANGLE', - # 'POINTING_DIRECTION', + 'POINTING_DIRECTION', 'SUB_SOLAR_POINT', 'SUB_OBSERVER_POINT', 'SURFACE_INTERCEPT_POINT', @@ -171,9 +171,22 @@ 'Unitary', ], 'COORDINATE_REPRESENTATION': [ - 'LATITUDINAL', + 'RECTANGULAR', + 'RA_DEC', + 'LATITUDINAL', # (planetocentric) 'PLANETODETIC', 'PLANETOGRAPHIC', + 'CYLINDRICAL', + 'SPHERICAL', + 'AZ_EL', + ], + 'COORDINATE_REPRESENTATION_POINTING_DIRECTION': [ + 'RECTANGULAR', + 'RA_DEC', + 'LATITUDINAL', # (planetocentric) + 'CYLINDRICAL', + 'SPHERICAL', + 'AZ_EL', ], 'SUB_POINT_TYPE': [ 'Near point: ellipsoid', @@ -245,4 +258,8 @@ 'VECTOR_IN_REFERENCE_FRAME', 'INSTRUMENT_FOV_BOUNDARY_VECTORS', ], + 'VECTOR_MAGNITUDE': [ + 'UNIT', + 'PRESERVE_ORIGINAL', + ] } From cca13f1be09bc8da6615d4e6cd4a127d028c8549 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Wed, 26 Mar 2025 13:21:39 +0100 Subject: [PATCH 09/12] Add TangentPoint calculation --- docs/calculation.rst | 118 ++++++++++ docs/cli.rst | 1 + examples/calculation.ipynb | 87 ++++++- setup.py | 3 + tests/test_calculation.py | 86 ++++++- tests/test_cli.py | 38 +++- tests/test_pointing_direction.py | 2 +- tests/test_tangent_point.py | 101 +++++++++ webgeocalc/__init__.py | 12 +- webgeocalc/calculation.py | 378 +++++++++++++++++++++++++------ webgeocalc/calculation_types.py | 179 +++++++++++++++ webgeocalc/cli.py | 12 +- webgeocalc/vars.py | 25 +- 13 files changed, 939 insertions(+), 103 deletions(-) create mode 100644 tests/test_tangent_point.py diff --git a/docs/calculation.rst b/docs/calculation.rst index 7d90b39..727c14e 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -854,6 +854,124 @@ from an observer. .. autoclass:: SurfaceInterceptPoint +Tangent Point +------------- + +Calculate the tangent point for a given observer, ray emanating +from the observer, and target. + +The tangent point is defined as the point on the ray nearest to the target's surface. +This panel also computes the point on the target's surface nearest to +the tangent point. The locations of both points are optionally corrected for +light time and stellar aberration, and may be represented using either +Rectangular, Ra/Dec, Planetocentric, Planetodetic, Planetographic, Spherical or +Cylindrical coordinates. + +The target's surface shape is modeled as a triaxial ellipsoid. + +For remote sensing observations, for maximum accuracy, reception light time and +stellar aberration corrections should be used. These corrections model +observer-target-ray geometry as it is observed. + +For signal transmission applications, for maximum accuracy, transmission light +time and stellar aberration corrections should be used. These corrections model +the observer-target-ray geometry that applies to the transmitted signal. +For example, these corrections are needed to calculate the minimum altitude +of the signal's path over the target body. + +This calculation ignores differential aberration effects over the target +body's surface: it computes corrections only at a user-specified point, +which is called the "aberration correction locus." +The user may select either the **Tangent point** or corresponding **Surface point** +as the locus. In many cases, the differences between corrections for +these points are very small. + +Additionally, the illumination angles (incidence, emission and phase), +time and local true solar time at the target point (where the aberration +correction locus is set -- or at the tangent point if no corrections are used), +and the light time to that point, are computed. + +.. testsetup:: + + from webgeocalc import TangentPoint + +>>> TangentPoint( +... kernels = 5, +... times = '2010-11-30T14:02:00', +... target = 'ENCELADUS', +... target_frame = 'IAU_ENCELADUS', +... computation_method = 'ELLIPSOID', +... observer = 'CASSINI', +... direction_vector_type = 'INSTRUMENT_BORESIGHT', +... direction_instrument = 'CASSINI_UVIS_HSP', +... aberration_correction = 'CN+S', +... correction_locus = 'TANGENT_POINT', +... coordinate_representation = 'RECTANGULAR', +... verbose = False, +... ).run() +{'DATE': '2010-11-30 14:02:00.000000 UTC', + 'TANGENT_POINT_GEOMETRY': 'Ray above limb', + 'TANGENT_POINT_X': -9.48919159, + 'TANGENT_POINT_Y': 36.16728915, + 'TANGENT_POINT_Z': -330.78398864, + 'SURFACE_POINT_X': -7.19366254, + 'SURFACE_POINT_Y': 27.15497998, + 'SURFACE_POINT_Z': -247.12357543, + 'DISTANCE_TO_TANGENT_POINT': 49830.33175196, + 'TANGENT_POINT_ALTITUDE': 84.17574418, + 'INCIDENCE_ANGLE': 97.72660812, + 'EMISSION_ANGLE': 90.0185557, + 'PHASE_ANGLE': 14.11694399, + 'TIME_AT_POINT': '2010-11-30 14:01:59.833784 UTC', + 'LIGHT_TIME': 0.1662161, + 'LTST': '05:38:33'} + + +.. important:: + + Calculation required parameters: + - :py:attr:`~Calculation.kernels` or/and :py:attr:`~Calculation.kernel_paths` + - :py:attr:`~Calculation.times` or :py:attr:`~Calculation.intervals` with :py:attr:`~Calculation.time_step` and :py:attr:`~Calculation.time_step_units` + - :py:attr:`~Calculation.target` + - :py:attr:`~Calculation.target_frame` + - :py:attr:`~Calculation.observer` + - :py:attr:`~Calculation.direction_vector_type` (see below the additional required parameters) + + Default parameters: + - :py:attr:`~Calculation.computation_method`: ``ELLIPSOID`` + - :py:attr:`~Calculation.vector_ab_corr`: ``NONE`` (only used for ``VECTOR_IN_REFERENCE_FRAME``) + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.coordinate_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + + Additional required parameters for :py:attr:`~Calculation.direction_vector_type` is ``INSTRUMENT_BORESIGHT`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS``: + - :py:attr:`~Calculation.direction_instrument` + + Additional required parameters for :py:attr:`~Calculation.direction_vector_type` is ``REFERENCE_FRAME_AXIS``: + - :py:attr:`~Calculation.direction_frame` + - :py:attr:`~Calculation.direction_frame_axis` + + Additional required parameters for :py:attr:`~Calculation.direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV``: + - :py:attr:`~Calculation.direction_instrument` + - either :py:attr:`~Calculation.direction_vector_x`, :py:attr:`~Calculation.direction_vector_y` and :py:attr:`~Calculation.direction_vector_z` + - or :py:attr:`~Calculation.direction_vector_ra` and :py:attr:`~Calculation.direction_vector_dec` + - or :py:attr:`~Calculation.direction_vector_az`, :py:attr:`~Calculation.direction_vector_el`, :py:attr:`~Direction.azccw_flag` and :py:attr:`~Direction.elplsz_flag`, + + Additional required parameters for :py:attr:`~Calculation.direction_vector_type` is ``VECTOR_IN_REFERENCE_FRAME``: + - :py:attr:`~Calculation.vector_ab_corr` + - :py:attr:`~Calculation.direction_frame` + - either :py:attr:`~Calculation.direction_vector_x`, :py:attr:`~Calculation.direction_vector_y` and :py:attr:`~Calculation.direction_vector_z` + - or :py:attr:`~Calculation.direction_vector_ra` and :py:attr:`~Calculation.direction_vector_dec` + - or :py:attr:`~Calculation.direction_vector_az`, :py:attr:`~Calculation.direction_vector_el`, :py:attr:`~Direction.azccw_flag` and :py:attr:`~Direction.elplsz_flag`, + + Additional required parameters for :py:attr:`~Calculation.direction_vector_type` is ``DIRECTION_TO_OBJECT``: + - :py:attr:`~Calculation.direction_object` + + +.. autoclass:: TangentPoint + + Osculating Elements ------------------- diff --git a/docs/cli.rst b/docs/cli.rst index 74f0ce4..92ab5c3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -248,6 +248,7 @@ Here is the list of all the calculation entry points available on the CLI: - ``wgc-subsolar-point`` - ``wgc-subobserver-point`` - ``wgc-surface-intercept-point`` +- ``wgc-tangent-point`` - ``wgc-osculating-elements`` - ``wgc-time-conversion`` - ``wgc-gf-coordinate-search`` diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index 66a821c..62f4c1b 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -561,6 +561,15 @@ ").run()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pointing Direction\n", + "\n", + "Calculates the pointing direction in a user specified reference frame and output it as a unit or full magnitude vector represented in a user specified coordinate system: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#pointing-direction))" + ] + }, { "cell_type": "code", "execution_count": 12, @@ -778,6 +787,71 @@ ").run()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tangent Point\n", + "\n", + "Calculate the tangent point for a given observer, ray emanating from the observer, and target: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#tangent-point))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Calculation submit] Phase: COMPLETE (id: fec58854-edee-4eed-aa7e-767a9eb43fed)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'DATE': '2010-11-30 14:02:00.000000 UTC',\n", + " 'TANGENT_POINT_GEOMETRY': 'Ray above limb',\n", + " 'TANGENT_POINT_X': -9.48919159,\n", + " 'TANGENT_POINT_Y': 36.16728915,\n", + " 'TANGENT_POINT_Z': -330.78398864,\n", + " 'SURFACE_POINT_X': -7.19366254,\n", + " 'SURFACE_POINT_Y': 27.15497998,\n", + " 'SURFACE_POINT_Z': -247.12357543,\n", + " 'DISTANCE_TO_TANGENT_POINT': 49830.33175196,\n", + " 'TANGENT_POINT_ALTITUDE': 84.17574418,\n", + " 'INCIDENCE_ANGLE': 97.72660812,\n", + " 'EMISSION_ANGLE': 90.0185557,\n", + " 'PHASE_ANGLE': 14.11694399,\n", + " 'TIME_AT_POINT': '2010-11-30 14:01:59.833784 UTC',\n", + " 'LIGHT_TIME': 0.1662161,\n", + " 'LTST': '05:38:33'}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from webgeocalc import TangentPoint\n", + "\n", + "TangentPoint(\n", + " kernels = 5,\n", + " times = '2010-11-30T14:02:00',\n", + " computation_method = 'ELLIPSOID',\n", + " target = 'ENCELADUS',\n", + " target_frame = 'IAU_ENCELADUS',\n", + " observer = 'CASSINI',\n", + " direction_vector_type = 'INSTRUMENT_BORESIGHT',\n", + " direction_instrument = 'CASSINI_UVIS_HSP',\n", + " aberration_correction = 'CN+S',\n", + " correction_locus = 'TANGENT_POINT',\n", + " coordinate_representation = 'RECTANGULAR',\n", + ").run()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -790,14 +864,13 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: LOADING_KERNELS (id: 9d7686a6-4f73-4853-ab4f-1ed887f924ce)\n", "[Calculation update] Phase: COMPLETE (id: 9d7686a6-4f73-4853-ab4f-1ed887f924ce)\n" ] }, @@ -817,7 +890,7 @@ " 'CENTER_BODY_GM': 37931207.49865224}" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -844,7 +917,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -860,7 +933,7 @@ "{'DATE': '1/1729329441.004', 'DATE2': '2012-10-19 08:24:02.919085 UTC'}" ] }, - "execution_count": 17, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -888,7 +961,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -904,7 +977,7 @@ "{'DATE': '2012-10-19 08:39:33.814938 UTC', 'DURATION': 3394.11539114}" ] }, - "execution_count": 18, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } diff --git a/setup.py b/setup.py index 1deb0fb..c786788 100644 --- a/setup.py +++ b/setup.py @@ -54,9 +54,12 @@ 'wgc-angular-size = webgeocalc.cli:cli_angular_size', 'wgc-frame-transformation = webgeocalc.cli:cli_frame_transformation', 'wgc-illumination-angles = webgeocalc.cli:cli_illumination_angles', + 'wgc-phase-angle = webgeocalc.cli:cli_phase_angle', + 'wgc-pointing-direction = webgeocalc.cli:cli_pointing_direction', 'wgc-subsolar-point = webgeocalc.cli:cli_subsolar_point', 'wgc-subobserver-point = webgeocalc.cli:cli_subobserver_point', 'wgc-surface-intercept-point = webgeocalc.cli:cli_surface_intercept_point', + 'wgc-tangent-point = webgeocalc.cli:cli_tangent_point', 'wgc-osculating-elements = webgeocalc.cli:cli_osculating_elements', 'wgc-time-conversion = webgeocalc.cli:cli_time_conversion', 'wgc-gf-coordinate-search = webgeocalc.cli:cli_gf_coordinate_search', diff --git a/tests/test_calculation.py b/tests/test_calculation.py index 23d26b0..a517a4c 100644 --- a/tests/test_calculation.py +++ b/tests/test_calculation.py @@ -256,6 +256,39 @@ def test_calculation_aberration_correction_error(params): Calculation(aberration_correction='WRONG', **params) +def test_calculation_vector_ab_corr_error(params): + """Test type of aberration correction errors.""" + # Missing `direction_vector_type` + with raises(CalculationUndefinedAttr, match='direction_vector_type'): + Calculation(vector_ab_corr='NONE', **params) + + # Invalid `direction_vector_type` (not `VECTOR_IN_REFERENCE_FRAME`) + with raises(CalculationIncompatibleAttr, match='VECTOR_IN_REFERENCE_FRAME'): + Calculation(direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + vector_ab_corr='NONE', **params) + + # Invalid attribute + with raises(CalculationInvalidAttr): + Calculation(direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_ra=210, + direction_vector_dec=-10, + vector_ab_corr='WRONG', **params) + + +def test_calculation_correction_locus_error(params): + """Test aberration correction locus errors.""" + with raises(CalculationInvalidAttr): + Calculation(correction_locus='WRONG', **params) + + +def test_calculation_computation_method_error(params): + """Test computation method (for TANGENT POINT) errors.""" + with raises(CalculationInvalidAttr): + Calculation(computation_method='WRONG', **params) + + def test_calculation_state_representation_error(params): """Test error if state representation is invalid.""" with raises(CalculationInvalidAttr): @@ -305,13 +338,38 @@ def test_calculation_angular_velocity_units_error(params): def test_calculation_direction_vector_type_error(params): - """Test error if intercept vector type is invalid.""" + """Test error if direction vector type is invalid.""" with raises(CalculationInvalidAttr): Calculation(direction_vector_type='WRONG', **params) + # Missing `direction_object` attribute + with raises(CalculationRequiredAttr, match='direction_object'): + Calculation(direction_vector_type='DIRECTION_TO_OBJECT', **params) + + # DIRECTION_TO_OBJECT can only be used with `TANGENT_POINT` calculation + with raises(CalculationIncompatibleAttr, match='direction_vector_type'): + Calculation( + direction_vector_type='DIRECTION_TO_OBJECT', + direction_object='ANY', + **params + ) + + +def test_calculation_direction_object_error(params): + """Test errors when direction object is invalid.""" + # Missing `direction_vector_type` + with raises(CalculationUndefinedAttr, match='direction_vector_type'): + Calculation(direction_object='CASSINI_ISS_NAC', **params) + + # Invalid `direction_vector_type` (only `DIRECTION_TO_OBJECT`) + with raises(CalculationIncompatibleAttr, match='direction_object'): + Calculation(direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + direction_object='ANY', **params) + def test_calculation_direction_instrument_error(params): - """Test errors when intercept instrument is invalid.""" + """Test errors when direction instrument is invalid.""" with raises(CalculationUndefinedAttr): # Missing `direction_vector_type` Calculation(direction_instrument='CASSINI_ISS_NAC', **params) @@ -323,7 +381,7 @@ def test_calculation_direction_instrument_error(params): def test_calculation_direction_frame_error(params): - """Test errors when intercept frame is invalid.""" + """Test errors when direction frame is invalid.""" with raises(CalculationUndefinedAttr): # Missing 'direction_vector_type' Calculation(direction_frame='CASSINI_ISS_NAC', **params) @@ -335,7 +393,7 @@ def test_calculation_direction_frame_error(params): def test_calculation_direction_frame_axis_error(params): - """Test errors when intercept frame axis is invalid.""" + """Test errors when direction frame axis is invalid.""" with raises(CalculationUndefinedAttr): # Missing 'direction_frame' Calculation(direction_frame_axis='Z', **params) @@ -352,7 +410,7 @@ def test_calculation_direction_frame_axis_error(params): def test_calculation_direction_vector_error(params): - """Test errors when intercept vector is invalid.""" + """Test errors when direction vector is invalid.""" with raises(CalculationUndefinedAttr): # Missing 'direction_vector_type' Calculation(direction_vector_x=0, **params) @@ -363,6 +421,24 @@ def test_calculation_direction_vector_error(params): direction_vector_type='INSTRUMENT_BORESIGHT', **params) +def test_calculation_direction_vector_az_el_error(params): + """Test errors when direction vector azimuth/elevation are invalid.""" + with raises(CalculationRequiredAttr, match='azccw_flag'): + Calculation(direction_vector_az='WRONG', **params) + + with raises(CalculationRequiredAttr, match='elplsz_flag'): + Calculation(direction_vector_el='WRONG', **params) + + +def test_calculation_azccw_elplsz_flag_error(params): + """Test errors when azimuth or elevation flags are invalid.""" + with raises(CalculationInvalidAttr): + Calculation(azccw_flag='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(elplsz_flag='WRONG', **params) + + def test_calculation_output_time_system_error(params): """Test errors when output time system is invalid.""" with raises(CalculationInvalidAttr): diff --git a/tests/test_cli.py b/tests/test_cli.py index 33a6920..f965cd8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -3,11 +3,11 @@ from webgeocalc.cli import (_params, cli_angular_separation, cli_angular_size, cli_bodies, cli_frame_transformation, cli_frames, cli_gf_coordinate_search, cli_illumination_angles, - cli_instruments, cli_kernel_sets, - cli_osculating_elements, cli_phase_angle, - cli_pointing_direction, cli_state_vector, + cli_instruments, cli_kernel_sets, cli_osculating_elements, + cli_phase_angle, cli_pointing_direction, cli_state_vector, cli_subobserver_point, cli_subsolar_point, - cli_surface_intercept_point, cli_time_conversion) + cli_surface_intercept_point, cli_tangent_point, + cli_time_conversion) def test_cli_kernel_sets(capsys): @@ -473,6 +473,36 @@ def test_cli_surface_intercept_point_dry_run(capsys): assert "calculationType: SURFACE_INTERCEPT_POINT," in captured.out +def test_cli_tangent_point_dry_run(capsys): + """Test dry-run tangent point calculation parameter with the CLI.""" + argv = [ + '--dry-run', + '--kernels', '5', + '--times', '2010-11-30T14:02:00', + '--target', 'ENCELADUS', + '--target_frame', 'IAU_ENCELADUS', + '--computation_method', 'ELLIPSOID', + '--observer', 'CASSINI', + '--direction_vector_type', 'INSTRUMENT_BORESIGHT', + '--direction_instrument', 'CASSINI_UVIS_HSP', + '--aberration_correction', 'CN+S', + '--correction_locus', 'TANGENT_POINT', + ] + + cli_tangent_point(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: TANGENT_POINT," in captured.out + assert "target: ENCELADUS" in captured.out + assert "targetFrame: IAU_ENCELADUS" in captured.out + assert "observer: CASSINI" in captured.out + assert "directionVectorType: INSTRUMENT_BORESIGHT" in captured.out + assert "directionInstrument: CASSINI_UVIS_HSP" in captured.out + assert "aberrationCorrection: CN+S" in captured.out + assert "correctionLocus: TANGENT_POINT" in captured.out + assert "coordinateRepresentation: RECTANGULAR" in captured.out + + def test_cli_osculating_elements_dry_run(capsys): """Test dry-run osculating elements calculation parameter with the CLI.""" argv = ('--dry-run ' diff --git a/tests/test_pointing_direction.py b/tests/test_pointing_direction.py index c7ce731..9669355 100644 --- a/tests/test_pointing_direction.py +++ b/tests/test_pointing_direction.py @@ -23,7 +23,7 @@ def test_pointing_direction_payload(): coordinate_representation='AZ_EL', azccw_flag='FALSE', elplsz_flag='false', - ).payload == { + ) == { "kernels": [{"type": "KERNEL_SET", "id": 5}], "timeSystem": "UTC", "timeFormat": "CALENDAR", diff --git a/tests/test_tangent_point.py b/tests/test_tangent_point.py new file mode 100644 index 0000000..ffe8869 --- /dev/null +++ b/tests/test_tangent_point.py @@ -0,0 +1,101 @@ +"""Test WGC tangent point calculation.""" + +from pytest import raises + +from webgeocalc import TangentPoint +from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr + + +def test_pointing_direction_payload(): + """Test tangent point payload.""" + assert TangentPoint( + kernels=5, + times='2012-11-30T14:02:00', + target='ENCELADUS', + target_frame='IAU_ENCELADUS', + observer='CASSINI', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_az=210, + direction_vector_el=10, + azccw_flag='True', + elplsz_flag=False, + aberration_correction='CN+S', + correction_locus='TANGENT_POINT', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": ['2012-11-30T14:02:00'], + "target": "ENCELADUS", + "targetFrame": "IAU_ENCELADUS", + "observer": "CASSINI", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "J2000", + "directionVectorAz": 210, + "directionVectorEl": 10, + "azccwFlag": True, + "elplszFlag": False, + "correctionLocus": "TANGENT_POINT", + "calculationType": "TANGENT_POINT", + "computationMethod": "ELLIPSOID", + "vectorAbCorr": "NONE", # Appended only for `VECTOR_IN_REFERENCE_FRAME` + "aberrationCorrection": "CN+S", + "coordinateRepresentation": "RECTANGULAR", + } + + # With direction to object + assert TangentPoint( + kernels=5, + times='2012-11-30T14:02:00', + target='ENCELADUS', + target_frame='IAU_ENCELADUS', + observer='CASSINI', + direction_vector_type='DIRECTION_TO_OBJECT', + direction_object='SATURN', + aberration_correction='NONE', + correction_locus='SURFACE_POINT', + coordinate_representation='SPHERICAL', + ) == { + "kernels": [{"type": "KERNEL_SET", "id": 5}], + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": ['2012-11-30T14:02:00'], + "target": "ENCELADUS", + "targetFrame": "IAU_ENCELADUS", + "observer": "CASSINI", + "directionVectorType": "DIRECTION_TO_OBJECT", + "directionObject": "SATURN", + "correctionLocus": "SURFACE_POINT", + "calculationType": "TANGENT_POINT", + "computationMethod": "ELLIPSOID", + "aberrationCorrection": "NONE", + "coordinateRepresentation": "SPHERICAL", + } + + +def test_tangent_point_errors(): + """Test tangent point errors.""" + # Requires `correction_locus` for aberration correction not None + with raises(CalculationRequiredAttr, match='correction_locus'): + TangentPoint( + kernels=5, + times='2012-11-30T14:02:00', + target='ENCELADUS', + target_frame='IAU_ENCELADUS', + observer='CASSINI', + direction_vector_type='INSTRUMENT_BORESIGHT', + ) + + # Invalid `coordinate_representation` (PLANETODETIC is excluded) + with raises(CalculationInvalidAttr, match='coordinate_representation'): + TangentPoint( + kernels=5, + times='2012-11-30T14:02:00', + target='ENCELADUS', + target_frame='IAU_ENCELADUS', + observer='CASSINI', + direction_vector_type='INSTRUMENT_BORESIGHT', + aberration_correction='NONE', + coordinate_representation='WRONG', + ) diff --git a/webgeocalc/__init__.py b/webgeocalc/__init__.py index 96f6489..2132abe 100644 --- a/webgeocalc/__init__.py +++ b/webgeocalc/__init__.py @@ -2,11 +2,11 @@ from .api import API, Api, ESA_API, JPL_API from .calculation import Calculation -from .calculation_types import (AngularSeparation, AngularSize, - FrameTransformation, GFCoordinateSearch, - IlluminationAngles, OsculatingElements, PhaseAngle, - PointingDirection, StateVector, SubObserverPoint, - SubSolarPoint, SurfaceInterceptPoint, TimeConversion) +from .calculation_types import (AngularSeparation, AngularSize, FrameTransformation, + GFCoordinateSearch, IlluminationAngles, + OsculatingElements, PhaseAngle, PointingDirection, + StateVector, SubObserverPoint, SubSolarPoint, + SurfaceInterceptPoint, TangentPoint, TimeConversion) from .version import __version__ @@ -26,7 +26,7 @@ 'SubSolarPoint', 'SubObserverPoint', 'SurfaceInterceptPoint', - # 'TangentPoint', + 'TangentPoint', 'OsculatingElements', 'GFCoordinateSearch', # 'GFAngularSeparationSearch' diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index c3a3203..893c2c9 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -96,12 +96,18 @@ class Calculation(Payload): See: :py:attr:`center_body` aberration_correction: str See: :py:attr:`aberration_correction` + vector_ab_corr: str + See: :py:attr:`vector_ab_corr` + correction_locus: str + See: :py:attr:`correction_locus` spec_type: str See: :py:attr:`spec_type` direction_1: str See: :py:attr:`direction_1` direction_2: str See: :py:attr:`direction_2` + computation_method: str + See: :py:attr:`computation_method` state_representation: str See: :py:attr:`state_representation` time_location: str @@ -128,8 +134,10 @@ class Calculation(Payload): See: :py:attr:`longitude` sub_point_type: str See: :py:attr:`sub_point_type` - direction_vector_type: str + direction_vector_type: str or int See: :py:attr:`direction_vector_type` + direction_object: str + See: :py:attr:`direction_object` direction_instrument: str or int See: :py:attr:`direction_instrument` direction_frame: str @@ -146,6 +154,14 @@ class Calculation(Payload): See: :py:attr:`direction_vector_ra` direction_vector_dec: float See: :py:attr:`direction_vector_dec` + direction_vector_az: float + See :py:attr:`direction_vector_az` + direction_vector_el: float + See :py:attr:`direction_vector_el` + azccw_flag: bool or str + See :py:attr:`azccw_flag` + elplsz_flag: bool or str + See :py:attr:`elplsz_flag` Raises ------ @@ -1025,15 +1041,23 @@ def aberration_correction(self, val): aberration_correction: str The SPICE aberration correction string. One of: - - NONE - - LT - - LT+S - - CN - - CN+S - - XLT - - XLT+S - - XCN - - XCN+S + - ``NONE`` + - ``LT`` + - ``LT+S`` + - ``CN`` + - ``CN+S`` + - ``XLT`` + - ``XLT+S`` + - ``XCN`` + - ``XCN+S`` + + Warning + ------- + In ``TANGENT_POINT`` calculation, the selected aberration correction applies + both to the point set by :py:attr:`correction_locus` and the direction vector + if :py:attr:`direction_vector_type` is ``DIRECTION_TO_OBJECT``. + For any other :py:attr:`direction_vector_type`, the selected aberration correction + only applies to the point set by :py:attr:`correction_locus`. Raises ------ @@ -1043,6 +1067,93 @@ def aberration_correction(self, val): """ self.__aberrationCorrection = val + @parameter(only='VECTOR_AB_CORR') + def vector_ab_corr(self, val): + """Type of aberration correction. + + Parameters + ---------- + vector_ab_corr: str + Type of aberration correction to be applied to the specified vector. + Only required if :py:attr:`directionVectorType` is + ``VECTOR_IN_REFERENCE_FRAME``. One of: + + - ``NONE`` + - ``STELLAR_ABERRATION_VECTOR`` + + Note + ---- + Use ``NONE`` to compute geometry without aberration corrections, + and ``STELLAR_ABERRATION_VECTOR`` to correct the vector's direction + for stellar aberration, taking into account the velocity of the observer + with respect to the solar system barycenter. The direction of stellar + aberration correction is determined by the light time direction selected + in :py:attr:``aberration_correction``. + + For backward compatibility, if not provided, it is assumed to be ``NONE``. + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationUndefinedAttr + If :py:attr:`direction_vector_type` is not supplied. + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` is not ``VECTOR_IN_REFERENCE_FRAME``. + + """ + if 'direction_vector_type' not in self.params: + raise CalculationUndefinedAttr('vector_ab_corr', val, 'direction_vector_type') + + if self.params['direction_vector_type'] != 'VECTOR_IN_REFERENCE_FRAME': + raise CalculationIncompatibleAttr( + 'vector_ab_corr', val, 'direction_vector_type', + self.params['direction_vector_type'], ['VECTOR_IN_REFERENCE_FRAME']) + + self.__vectorAbCorr = val + + @parameter(only='CORRECTION_LOCUS') + def correction_locus(self, val): + """Aberration correction locus. + + Parameters + ---------- + correction_locus: str + Aberration correction *locus*, which is the fixed point in the reference frame + for which light time and stellar aberration corrections are computed. One of: + + - ``TANGENT_POINT`` + - ``SURFACE_POINT`` + + Required only when :py:attr:`aberration_correction` is not ``NONE``. + + Differential aberration effects across the surface of the target body are not + considered. When aberration corrections are used, the effective positions of + the observer and target, and the orientation of the target, are computed + according to the corrections determined for the aberration correction locus. + + The light time used to determine the position and orientation of the target + body is that between the aberration correction locus and the observer. + + The stellar aberration correction applied to the position of the target is + that computed for the aberration correction locus. + + Use ``TANGENT_POINT`` to compute corrections at the "tangent point." + Use ``SURFACE_POINT`` to compute corrections at the point on the target's + surface nearest to the tangent point. + + When :py:attr:`aberration_correction` is ``NONE``, the illumination angles, + time, local true solar time and light time are computed with respect to the + tangent point. + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__correctionLocus = val + @parameter(only='SPEC_TYPE') def spec_type(self, val): """Angular separation computation type. @@ -1128,6 +1239,20 @@ def vector_magnitude(self, val): """ self.__vectorMagnitude = val + @parameter(only='COMPUTATION_METHOD') + def computation_method(self, val): + """The computation method for TANGENT_POINT calculation. + + Only: + + - ``ELLIPSOID`` + + Currently, it is restricted to ELLIPSOID. + This value indicates that the target shape is modeled as a triaxial ellipsoid. + + """ + self.__computationMethod = val + @parameter(only='STATE_REPRESENTATION') def state_representation(self, val): """State representation. @@ -1393,7 +1518,7 @@ def angular_velocity_units(self, val): @parameter(only='COORDINATE_REPRESENTATION') def coordinate_representation(self, val): - """Coordinate Representation. + """Coordinate representation. Parameters ---------- @@ -1407,7 +1532,13 @@ def coordinate_representation(self, val): - ``PLANETOGRAPHIC`` (not for ``POINTING_DIRECTION``) - ``CYLINDRICAL`` - ``SPHERICAL`` - - ``AZ_EL`` + - ``AZ_EL`` (not for ``TANGENT_POINT``) + + Note + ---- + For ``TANGENT_POINT`` calculation, it corresponds to the + coordinate system to represent both the Tangent point and + the point on the surface's target nearest to the target point. Raises ------ @@ -1457,56 +1588,6 @@ def longitude(self, val): else: raise CalculationInvalidValue('longitude', val, -180, 180) - @parameter(only='BOOLEAN') - def azccw_flag(self, val): - """Flag indicating how azimuth is measured. - - If ``azccw_flag`` is ``True``, azimuth increases in the counterclockwise - direction; otherwise it increases in the clockwise direction. - - Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL``. - - Parameters - ---------- - azccw_flag: bool or str - Azimuth orientation. - - Raises - ------ - CalculationInvalidValue - If ``azccw_flag`` not a boolean. - - """ - if isinstance(val, str): - val = val.upper() == 'TRUE' - - self.__azccwFlag = val - - @parameter(only='BOOLEAN') - def elplsz_flag(self, val): - """Flag indicating how elevation is measured. - - If ``elplsz_flag`` is ``True``, elevation increases from the XY plane - toward +Z; otherwise toward -Z. - - Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL``. - - Parameters - ---------- - elplsz_flag: bool or str - Azimuth orientation. - - Raises - ------ - CalculationInvalidValue - If ``elplsz_flag`` not a boolean. - - """ - if isinstance(val, str): - val = val.upper() == 'TRUE' - - self.__elplszFlag = val - @parameter(only='SUB_POINT_TYPE') def sub_point_type(self, val): """Sub-observer point. @@ -1537,15 +1618,22 @@ def direction_vector_type(self, val): Parameters ---------- direction_vector_type: str - Type of vector to be used as the ray direction. One of: + Type of vector to be used as the ray direction: the instrument boresight + vector, the instrument field-of-view boundary vectors, an axis of the + specified reference frame, a vector in the reference frame of the specified + instrument, a vector in the specified reference frame, or a vector defined + by the position of a given object as seen from the observer. - - INSTRUMENT_BORESIGHT *(the instrument boresight vector)* - - INSTRUMENT_FOV_BOUNDARY_VECTORS *(the instrument field-of-view + One of: + + - ``INSTRUMENT_BORESIGHT`` *(the instrument boresight vector)* + - ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` *(the instrument field-of-view boundary vectors)* - - REFERENCE_FRAME_AXIS *(an axis of the specified reference frame)* - - VECTOR_IN_INSTRUMENT_FOV *(a vector in the reference frame of the + - ``REFERENCE_FRAME_AXIS`` *(an axis of the specified reference frame)* + - ``VECTOR_IN_INSTRUMENT_FOV`` *(a vector in the reference frame of the specified instrument)* - - VECTOR_IN_REFERENCE_FRAME *(a vector in the specified reference frame)* + - ``VECTOR_IN_REFERENCE_FRAME`` *(a vector in the specified reference frame)* + - ``DIRECTION_TO_OBJECT`` *(only for ``TANGENT_POINT`` calculation)* Raises ------ @@ -1561,6 +1649,12 @@ def direction_vector_type(self, val): CalculationRequiredAttr If this parameter is ``REFERENCE_FRAME_AXIS`` but :py:attr:`direction_frame_axis` is not provided. + CalculationRequiredAttr + If this parameter is ``DIRECTION_TO_OBJECT`` + but :py:attr:`direction_object` is not provided. + CalculationIncompatibleAttr + If this parameter is ``DIRECTION_TO_OBJECT`` but + but :py:attr:`calculation_type` is not ``TANGENT_POINT``. CalculationUndefinedAttr If this parameter is ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` but neither :py:attr:`direction_vector_x`, @@ -1571,14 +1665,33 @@ def direction_vector_type(self, val): """ self.__directionVectorType = val - if val in ['INSTRUMENT_BORESIGHT', 'INSTRUMENT_FOV_BOUNDARY_VECTORS', - 'VECTOR_IN_INSTRUMENT_FOV']: - self._required('direction_instrument') + match val: + case ( + 'INSTRUMENT_BORESIGHT' | + 'INSTRUMENT_FOV_BOUNDARY_VECTORS' | + 'VECTOR_IN_INSTRUMENT_FOV' + ): + self._required('direction_instrument') + + case ( + 'REFERENCE_FRAME_AXIS' | 'VECTOR_IN_REFERENCE_FRAME' + ): + self._required('direction_frame') + + if val == 'REFERENCE_FRAME_AXIS': + self._required('direction_frame_axis') + + case 'DIRECTION_TO_OBJECT': + self._required('direction_object') - elif val in ['REFERENCE_FRAME_AXIS', 'VECTOR_IN_REFERENCE_FRAME']: - self._required('direction_frame') - if val == 'REFERENCE_FRAME_AXIS': - self._required('direction_frame_axis') + if self.params['calculation_type'] != 'TANGENT_POINT': + raise CalculationIncompatibleAttr( + 'direction_vector_type', val, 'calculation_type', + self.params['calculation_type'], [ + v for v in VALID_PARAMETERS['DIRECTION_VECTOR_TYPE'] + if v != 'DIRECTION_TO_OBJECT' + ] + ) keys = self.params.keys() if val in ['VECTOR_IN_INSTRUMENT_FOV', 'VECTOR_IN_REFERENCE_FRAME']: @@ -1589,11 +1702,46 @@ def direction_vector_type(self, val): ) and not ( 'direction_vector_ra' in keys and 'direction_vector_dec' in keys + ) and not ( + 'direction_vector_az' in keys and + 'direction_vector_el' in keys and + 'azccw_flag' in keys and + 'elplsz_flag' in keys ): raise CalculationUndefinedAttr( 'direction_vector_type', val, "direction_vector_x/y/z' or 'direction_vector_ra/dec") + @parameter + def direction_object(self, val): + """Direction object. + + Parameters + ---------- + direction_object: str or int + The ephemeris object ``name`` or ``id``. + Required only if :py:attr:`direction_vector_type` + is ``DIRECTION_TO_OBJECT``. + + Raises + ------ + CalculationUndefinedAttr + If :py:attr:`direction_vector_type` is not provided. + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` not in ``DIRECTION_TO_OBJECT``. + + """ + if 'direction_vector_type' not in self.params: + raise CalculationUndefinedAttr( + 'direction_object', val, 'direction_vector_type') + + if self.params['direction_vector_type'] != 'DIRECTION_TO_OBJECT': + raise CalculationIncompatibleAttr( + 'direction_object', val, 'direction_vector_type', + self.params['direction_vector_type'], ['DIRECTION_TO_OBJECT']) + + self.__directionObject = val if isinstance(val, int) else val.upper() + @parameter def direction_instrument(self, val): """Direction instrument. @@ -1779,6 +1927,86 @@ def direction_vector_dec(self, val): """ self.__directionVectorDec = self.direction_vector('dec', val) + @parameter + def direction_vector_az(self, val): + """The azimuth ray's direction vector coordinate. + + Parameters + ---------- + direction_vector_az: float + Direction Azimuth-coordinate. See :py:func:`direction_vector`. + + """ + self._required('azccw_flag') + self.__directionVectorAz = self.direction_vector('az', val) + + @parameter + def direction_vector_el(self, val): + """The elevation ray's direction vector coordinate. + + Parameters + ---------- + direction_vector_el: float + Direction elevation-coordinate. See :py:func:`direction_vector`. + + """ + self._required('elplsz_flag') + self.__directionVectorEl = self.direction_vector('el', val) + + @parameter(only='BOOLEAN') + def azccw_flag(self, val): + """Flag indicating how azimuth is measured. + + If ``azccw_flag`` is ``True``, azimuth increases in the counterclockwise + direction; otherwise it increases in the clockwise direction. + + Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL`` + in ``POINTING_DIRECTION`` calculation or + :py:attr:`direction_vector_az` is set in ``TANGENT_POINT`` calculation. + + Parameters + ---------- + azccw_flag: bool or str + Azimuth orientation. + + Raises + ------ + CalculationInvalidValue + If ``azccw_flag`` not a boolean. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + self.__azccwFlag = val + + @parameter(only='BOOLEAN') + def elplsz_flag(self, val): + """Flag indicating how elevation is measured. + + If ``elplsz_flag`` is ``True``, elevation increases from the XY plane + toward +Z; otherwise toward -Z. + + Required only when :py:attr:`coordinate_representation` is set to ``AZ_EL`` + in ``POINTING_DIRECTION`` calculation or + :py:attr:`direction_vector_el` is set in ``TANGENT_POINT`` calculation. + + Parameters + ---------- + elplsz_flag: bool or str + Azimuth orientation. + + Raises + ------ + CalculationInvalidValue + If ``elplsz_flag`` not a boolean. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + self.__elplszFlag = val + @parameter(only='TIME_UNITS') def output_duration_units(self, val): """Output duration time units. diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index f4ab0bb..190e46c 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -770,6 +770,185 @@ def __init__(self, shape_1='ELLIPSOID', direction_vector_type='INSTRUMENT_BORESI super().__init__(**kwargs) +class TangentPoint(Calculation): + """Tangent point calculation. + + Calculate the tangent point for a given observer, ray emanating + from the observer, and target. + + The tangent point is defined as the point on the ray nearest to the target's surface. + This panel also computes the point on the target's surface nearest to + the tangent point. The locations of both points are optionally corrected for + light time and stellar aberration, and may be represented using either + Rectangular, Ra/Dec, Planetocentric, Planetodetic, Planetographic, Spherical or + Cylindrical coordinates. + + The target's surface shape is modeled as a triaxial ellipsoid. + + For remote sensing observations, for maximum accuracy, reception light time and + stellar aberration corrections should be used. These corrections model + observer-target-ray geometry as it is observed. + + For signal transmission applications, for maximum accuracy, transmission light + time and stellar aberration corrections should be used. These corrections model + the observer-target-ray geometry that applies to the transmitted signal. + For example, these corrections are needed to calculate the minimum altitude + of the signal's path over the target body. + + This calculation ignores differential aberration effects over the target + body's surface: it computes corrections only at a user-specified point, + which is called the "aberration correction locus." + The user may select either the **Tangent point** or corresponding **Surface point** + as the locus. In many cases, the differences between corrections for + these points are very small. + + Additionally, the illumination angles (incidence, emission and phase), + time and local true solar time at the target point (where the aberration + correction locus is set -- or at the tangent point if no corrections are used), + and the light time to that point, are computed. + + Parameters + ---------- + target: str or int + See: :py:attr:`target` + target_frame: str or int + See: :py:attr:`target_frame` + observer: str or int + See: :py:attr:`observer` + direction_vector_type: str + See: :py:attr:`direction_vector_type` + + direction_object: str or int + See: :py:attr:`direction_object`. + Only when :py:attr:`direction_vector_type` is ``DIRECTION_TO_OBJECT`` + + direction_instrument: str or int + See: :py:attr:`direction_instrument`. + Only when :py:attr:`direction_vector_type` is + ``INSTRUMENT_BORESIGHT, ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` + or ``VECTOR_IN_INSTRUMENT_FOV`` + + direction_frame: str + See: :py:attr:`direction_frame`. + Only when :py:attr:`direction_vector_type` is + ``REFERENCE_FRAME_AXIS`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_frame_axis: str + See: :py:attr:`direction_frame_axis`. + Only when :py:attr:`direction_vector_type` is + ``REFERENCE_FRAME_AXIS``. + + direction_vector_x: float + See: :py:attr:`direction_vector_x`. + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_y: float + See: :py:attr:`direction_vector_y`. + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_z: float + See: :py:attr:`direction_vector_z`. + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_ra: float + See: :py:attr:`direction_vector_ra` + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_dec: float + See: :py:attr:`direction_vector_dec` + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_az: float + See: :py:attr:`direction_vector_az` + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + direction_vector_el: float + See: :py:attr:`direction_vector_el` + Only when :py:attr:`direction_vector_type` is + ``VECTOR_IN_INSTRUMENT_FOV`` or ``VECTOR_IN_REFERENCE_FRAME`` + + azccw_flag: bool or str + See: :py:attr:`azccw_flag`. + Only when :py:attr:`direction_vector_az` is provided. + + elplsz_flag: bool or str + See: :py:attr:`elplsz_flag`. + Only when :py:attr:`direction_vector_el` is provided. + + computation_method: str, optional + See: :py:attr:`computation_method` (default: ``ELLIPSOID``) + vector_ab_corr: str, optional + See: :py:attr:`vector_ab_corr` (default: ``NONE``) + aberration_correction: str, optional + See: :py:attr:`aberration_correction` (default: ``CN``) + correction_locus: str, optional + See: :py:attr:`correction_locus` + (required if :py:attr:`aberration_correction` is not ``NONE``) + coordinate_representation: str, optional + See: :py:attr:`coordinate_representation` (default: ``RECTANGULAR``) + + Other Parameters + ---------------- + kernels: str, int, [str or/and int] + See: :py:attr:`kernels` + kernel_paths: str, [str] + See: :py:attr:`kernel_paths` + times: str or [str] + See: :py:attr:`times` + intervals: [str, str] or {'startTime': str, 'endTime': str} or [interval, ...] + See: :py:attr:`intervals` + time_step: int + See: :py:attr:`time_step` + time_step_units: str + See: :py:attr:`time_step_units` + time_system: str + See: :py:attr:`time_system` + time_format: str + See: :py:attr:`time_format` + + Raises + ------ + CalculationRequiredAttr + If :py:attr:`target`, :py:attr:`reference_frame`, :py:attr:`observer` + or :py:attr:`direction_vector_type` are not provided. + CalculationRequiredAttr + If :py:attr:`aberration_correction` is not ``NONE`` and + :py:attr:`correction_locus` is not provided. + + """ + + REQUIRED = ('target', 'target_frame', 'observer', 'direction_vector_type') + + def __init__(self, computation_method='ELLIPSOID', + vector_ab_corr='NONE', aberration_correction='CN', + coordinate_representation='RECTANGULAR', **kwargs): + + if aberration_correction != 'NONE': + self.REQUIRED += ('correction_locus',) + + valid = VALID_PARAMETERS['COORDINATE_REPRESENTATION_TANGENT_POINT'] + if coordinate_representation not in valid: + raise CalculationInvalidAttr( + 'coordinate_representation', coordinate_representation, valid) + + kwargs['calculation_type'] = 'TANGENT_POINT' + kwargs['computation_method'] = computation_method + + if kwargs.get('direction_vector_type') == 'VECTOR_IN_REFERENCE_FRAME': + kwargs['vector_ab_corr'] = vector_ab_corr + + kwargs['aberration_correction'] = aberration_correction + kwargs['coordinate_representation'] = coordinate_representation + + super().__init__(**kwargs) + + class OsculatingElements(Calculation): """Osculating elements calculation. diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 44af494..fda423d 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -4,11 +4,12 @@ import re from .api import Api, ESA_API, JPL_API -from .calculation_types import (AngularSeparation, AngularSize, - Calculation, FrameTransformation, GFCoordinateSearch, +from .calculation_types import (AngularSeparation, AngularSize, Calculation, + FrameTransformation, GFCoordinateSearch, IlluminationAngles, OsculatingElements, PhaseAngle, PointingDirection, StateVector, SubObserverPoint, - SubSolarPoint, SurfaceInterceptPoint, TimeConversion) + SubSolarPoint, SurfaceInterceptPoint, TangentPoint, + TimeConversion) from .errors import KernelSetNotFound, TooManyKernelSets @@ -350,6 +351,11 @@ def cli_surface_intercept_point(argv=None): cli_calculation(argv, SurfaceInterceptPoint, desc='Surface Intercept Point') +def cli_tangent_point(argv=None): + """Submit tangent point calculation with the CLI.""" + cli_calculation(argv, TangentPoint, desc='Tangent Point') + + def cli_osculating_elements(argv=None): """Submit osculating elements calculation with the CLI.""" cli_calculation(argv, OsculatingElements, desc='Osculating Elements') diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 3387e08..4acd3d7 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -22,7 +22,7 @@ 'SUB_SOLAR_POINT', 'SUB_OBSERVER_POINT', 'SURFACE_INTERCEPT_POINT', - # 'TANGENT_POINT', + 'TANGENT_POINT', 'OSCULATING_ELEMENTS', 'GF_COORDINATE_SEARCH', # 'GF_ANGULAR_SEPARATION_SEARCH', @@ -112,6 +112,14 @@ 'S', 'XS', ], + 'VECTOR_AB_CORR': [ + 'NONE', + 'STELLAR_ABERRATION_VECTOR', + ], + 'CORRECTION_LOCUS': [ + 'TANGENT_POINT', + 'SURFACE_POINT', + ], 'SPEC_TYPE': [ 'TWO_TARGETS', 'TWO_DIRECTIONS', @@ -188,6 +196,15 @@ 'SPHERICAL', 'AZ_EL', ], + 'COORDINATE_REPRESENTATION_TANGENT_POINT': [ + 'RECTANGULAR', + 'RA_DEC', + 'LATITUDINAL', # (planetocentric) + 'PLANETODETIC', + 'PLANETOGRAPHIC', + 'CYLINDRICAL', + 'SPHERICAL', + ], 'SUB_POINT_TYPE': [ 'Near point: ellipsoid', 'Intercept: ellipsoid', @@ -257,9 +274,13 @@ 'VECTOR_IN_INSTRUMENT_FOV', 'VECTOR_IN_REFERENCE_FRAME', 'INSTRUMENT_FOV_BOUNDARY_VECTORS', + 'DIRECTION_TO_OBJECT', ], 'VECTOR_MAGNITUDE': [ 'UNIT', 'PRESERVE_ORIGINAL', - ] + ], + 'COMPUTATION_METHOD': [ + 'ELLIPSOID', + ], } From 90fc579865f9c0234b984b3135f0f88e18e27a03 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Wed, 26 Mar 2025 13:21:51 +0100 Subject: [PATCH 10/12] fix API version in tests --- tests/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index ff39997..c838d77 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -140,8 +140,8 @@ def test_api_metadata(): assert API['documentation'] == \ 'https://wgc2.jpl.nasa.gov:8443/webgeocalc/documents/api-info.html' assert API['contact'] == 'Boris Semenov ' - assert API['version'] == '2.7.6' - assert API['build_id'] == '5363 N67 29-JAN-2025' + assert API['version'] == '2.8.2' + assert API['build_id'] == '5392 N67 24-MAR-2025' with raises(KeyError): _ = API['foo'] From 19fe5cac4e8a7b096f7edc81b81b1e2858e5442a Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Wed, 26 Mar 2025 13:22:36 +0100 Subject: [PATCH 11/12] Expend invalid attribute tests on Calculation and fix doc strings --- docs/params.rst | 19 ++++--- tests/test_calculation.py | 84 +++++++++++++++++++++++++++++++ webgeocalc/calculation.py | 40 +++++++++++++-- webgeocalc/calculation_types.py | 88 ++++++++++++++++----------------- 4 files changed, 177 insertions(+), 54 deletions(-) diff --git a/docs/params.rst b/docs/params.rst index ee735df..826ccbd 100644 --- a/docs/params.rst +++ b/docs/params.rst @@ -109,19 +109,24 @@ object: ``INSTRUMENT_BORESIGHT``, ``REFERENCE_FRAME_AXIS``, ``VECTOR_IN_INSTRUMENT_FOV``, ``VECTOR_IN_REFERENCE_FRAME`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` - Direction ``VECTOR + INSTRUMENT_BORESIGHT`` required parameters: + Direction vector ``INSTRUMENT_BORESIGHT`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` required parameters: - :py:attr:`~Direction.direction_instrument` - Direction ``VECTOR + REFERENCE_FRAME_AXIS`` required parameters: + Direction vector ``REFERENCE_FRAME_AXIS`` required parameters: - :py:attr:`~Direction.direction_frame` - :py:attr:`~Direction.direction_frame_axis` - Direction ``VECTOR + VECTOR_IN_INSTRUMENT_FOV`` required parameters: + Direction vector ``VECTOR_IN_INSTRUMENT_FOV`` required parameters: - :py:attr:`~Direction.direction_instrument` - - :py:attr:`~Direction.direction_vector_x`, :py:attr:`~Direction.direction_vector_y` and :py:attr:`~Direction.direction_vector_z` - or :py:attr:`~Direction.direction_vector_ra` and :py:attr:`~Direction.direction_vector_dec` - or :py:attr:`~Direction.direction_vector_az`, :py:attr:`~Direction.direction_vector_el`, :py:attr:`~Direction.azccw_flag` - and :py:attr:`~Direction.elplsz_flag`, + - either :py:attr:`~Direction.direction_vector_x`, :py:attr:`~Direction.direction_vector_y` and :py:attr:`~Direction.direction_vector_z` + - or :py:attr:`~Direction.direction_vector_ra` and :py:attr:`~Direction.direction_vector_dec` + - or :py:attr:`~Direction.direction_vector_az`, :py:attr:`~Direction.direction_vector_el`, :py:attr:`~Direction.azccw_flag` and :py:attr:`~Direction.elplsz_flag`, + + Direction vector ``VECTOR_IN_REFERENCE_FRAME`` required parameters: + - :py:attr:`~Direction.direction_frame` + - either :py:attr:`~Direction.direction_vector_x`, :py:attr:`~Direction.direction_vector_y` and :py:attr:`~Direction.direction_vector_z` + - or :py:attr:`~Direction.direction_vector_ra` and :py:attr:`~Direction.direction_vector_dec` + - or :py:attr:`~Direction.direction_vector_az`, :py:attr:`~Direction.direction_vector_el`, :py:attr:`~Direction.azccw_flag` and :py:attr:`~Direction.elplsz_flag`, Default parameters: - :py:attr:`~Direction.aberration_correction`: ``NONE`` diff --git a/tests/test_calculation.py b/tests/test_calculation.py index a517a4c..838141b 100644 --- a/tests/test_calculation.py +++ b/tests/test_calculation.py @@ -283,6 +283,18 @@ def test_calculation_correction_locus_error(params): Calculation(correction_locus='WRONG', **params) +def test_calculation_spec_type_error(params): + """Test angular separation computation type errors.""" + with raises(CalculationInvalidAttr): + Calculation(spec_type='WRONG', **params) + + +def test_calculation_vector_magnitude_error(params): + """Test vector magnitude errors.""" + with raises(CalculationInvalidAttr): + Calculation(vector_magnitude='WRONG', **params) + + def test_calculation_computation_method_error(params): """Test computation method (for TANGENT POINT) errors.""" with raises(CalculationInvalidAttr): @@ -295,6 +307,18 @@ def test_calculation_state_representation_error(params): Calculation(state_representation='WRONG', **params) +def test_calculation_time_location_error(params): + """Test frame for the input times errors.""" + with raises(CalculationInvalidAttr): + Calculation(time_location='WRONG', **params) + + +def test_calculation_orientation_representation_error(params): + """Test representation of the result transformation errors.""" + with raises(CalculationInvalidAttr): + Calculation(orientation_representation='WRONG', **params) + + def test_calculation_shape_error(params): """Test error if shape 1-2 is invalid.""" with raises(CalculationInvalidAttr): @@ -316,6 +340,9 @@ def test_calculation_axis_error(params): def test_calculation_angular_units_error(params): """Test errors if angular units are invalid.""" + with raises(CalculationInvalidAttr): + Calculation(angular_units='WRONG', **params) + with raises(CalculationUndefinedAttr): Calculation(angular_units='deg', **params) # Missing `orientation_representation` @@ -325,8 +352,17 @@ def test_calculation_angular_units_error(params): angular_units='deg', **params) +def test_calculation_angular_velocity_representation_error(params): + """Test angular velocity representation errors.""" + with raises(CalculationInvalidAttr): + Calculation(angular_velocity_representation='WRONG', **params) + + def test_calculation_angular_velocity_units_error(params): """Test errors with angular velocity units.""" + with raises(CalculationInvalidAttr): + Calculation(angular_velocity_units='WRONG', **params) + with raises(CalculationUndefinedAttr): # Missing `angular_velocity_representation` Calculation(angular_velocity_units='deg/s', **params) @@ -337,6 +373,18 @@ def test_calculation_angular_velocity_units_error(params): angular_velocity_units='deg/s', **params) +def test_calculation_coordinate_representation_error(params): + """Test Coordinate representation errors.""" + with raises(CalculationInvalidAttr): + Calculation(coordinate_representation='WRONG', **params) + + +def test_calculation_sub_point_type_error(params): + """Test sub-observer point errors.""" + with raises(CalculationInvalidAttr): + Calculation(sub_point_type='WRONG', **params) + + def test_calculation_direction_vector_type_error(params): """Test error if direction vector type is invalid.""" with raises(CalculationInvalidAttr): @@ -439,6 +487,42 @@ def test_calculation_azccw_elplsz_flag_error(params): Calculation(elplsz_flag='WRONG', **params) +def test_calculation_output_duration_units_error(params): + """Test errors when output duration units is invalid.""" + with raises(CalculationInvalidAttr): + Calculation(output_duration_units='WRONG', **params) + + +def test_calculation_interval_adjustment_errors(params): + """Test errors when interval parameters are invalid.""" + with raises(CalculationInvalidAttr): + Calculation(interval_adjustment='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(interval_adjustment_units='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(interval_filtering='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(interval_filtering='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(interval_filtering_threshold_units='WRONG', **params) + + +def test_calculation_coordinate_errors(params): + """Test errors when coordinate are invalid.""" + with raises(CalculationInvalidAttr): + Calculation(coordinate_system='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(coordinate='WRONG', **params) + + with raises(CalculationInvalidAttr): + Calculation(relational_condition='WRONG', **params) + + def test_calculation_output_time_system_error(params): """Test errors when output time system is invalid.""" with raises(CalculationInvalidAttr): diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 893c2c9..cda5871 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -84,6 +84,8 @@ class Calculation(Payload): See: :py:attr:`shape_2` observer: str or int See: :py:attr:`observer` + illuminator: str + See: :py:attr:`illuminator` reference_frame: str or int See: :py:attr:`reference_frame` frame_1: str ot int @@ -102,10 +104,14 @@ class Calculation(Payload): See: :py:attr:`correction_locus` spec_type: str See: :py:attr:`spec_type` - direction_1: str + direction: dict or Direction + See: :py:attr:`direction` + direction_1: dict or Direction See: :py:attr:`direction_1` - direction_2: str + direction_2: dict or Direction See: :py:attr:`direction_2` + vector_magnitude: str + See: :py:attr:`vector_magnitude` computation_method: str See: :py:attr:`computation_method` state_representation: str @@ -162,6 +168,34 @@ class Calculation(Payload): See :py:attr:`azccw_flag` elplsz_flag: bool or str See :py:attr:`elplsz_flag` + output_duration_units: float + See :py:attr:`output_duration_units` + should_complement_window: bool + See :py:attr:`should_complement_window` + interval_adjustment: str + See :py:attr:`interval_adjustment` + interval_adjustment_amount: float + See :py:attr:`interval_adjustment_amount` + interval_adjustment_units: str + See :py:attr:`interval_adjustment_units` + interval_filtering: str + See :py:attr:`interval_filtering` + interval_filtering_threshold: float + See :py:attr:`interval_filtering_threshold` + interval_filtering_threshold_units: str + See :py:attr:`interval_filtering_threshold_units` + coordinate_system: str + See :py:attr:`coordinate_system` + coordinate: str + See :py:attr:`coordinate` + relational_condition: str + See :py:attr:`relational_condition` + reference_value: float + See :py:attr:`reference_value` + upper_limit: float + See :py:attr:`upper_limit` + adjustment_value: float + See :py:attr:`adjustment_value` Raises ------ @@ -966,7 +1000,7 @@ def illuminator(self, val): Parameters ---------- illuminator: str or int - The observing body ``name`` or ``id`` from :py:func:`API.bodies`. + The illumination body ``name`` or ``id`` from :py:func:`API.bodies`. """ self.__illuminator = val if isinstance(val, int) else val.upper() diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index 190e46c..9079cd4 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -234,21 +234,21 @@ class FrameTransformation(Calculation): angular_velocity_units: str, optional See: :py:attr:`angular_velocity_units` - Warning - ------- - Attribute :py:attr:`aberration_correction` must be ``NONE``, `LT``, ``CN``, - ``XLT`` or ``XCN``. + Warning + ------- + Attribute :py:attr:`aberration_correction` must be ``NONE``, `LT``, ``CN``, + ``XLT`` or ``XCN``. - Attributes :py:attr:`axis_1`, :py:attr:`axis_2` and :py:attr:`axis_3` - are imported only if :py:attr:`orientation_representation` is ``EULER_ANGLES``. + Attributes :py:attr:`axis_1`, :py:attr:`axis_2` and :py:attr:`axis_3` + are imported only if :py:attr:`orientation_representation` is ``EULER_ANGLES``. - Attribute :py:attr:`angular_units` is imported only - if :py:attr:`orientation_representation` is ``EULER_ANGLES`` - or ``ANGLE_AND_AXIS``. + Attribute :py:attr:`angular_units` is imported only + if :py:attr:`orientation_representation` is ``EULER_ANGLES`` + or ``ANGLE_AND_AXIS``. - Attribute :py:attr:`angular_velocity_units` is imported only if - :py:attr:`angular_velocity_representation` is ``VECTOR_IN_FRAME1``, - ``VECTOR_IN_FRAME2`` or ``EULER_ANGLE_DERIVATIVES``. + Attribute :py:attr:`angular_velocity_units` is imported only if + :py:attr:`angular_velocity_representation` is ``VECTOR_IN_FRAME1``, + ``VECTOR_IN_FRAME2`` or ``EULER_ANGLE_DERIVATIVES``. Other Parameters ---------------- @@ -548,10 +548,10 @@ class SubSolarPoint(Calculation): state_representation: str, optional See: :py:attr:`state_representation` - Warning - ------- - Attribute :py:attr:`aberration_correction` must be ``NONE``, `LT``, ``LT+S``, - ``CN`` or ``CN+S``. + Warning + ------- + Attribute :py:attr:`aberration_correction` must be ``NONE``, `LT``, ``LT+S``, + ``CN`` or ``CN+S``. Other Parameters ---------------- @@ -725,21 +725,21 @@ class SurfaceInterceptPoint(Calculation): direction_vector_dec: float See: :py:attr:`direction_vector_dec` - Warnings - -------- - Attributes :py:attr:`direction_instrument` is needed only if - :py:attr:`direction_vector_type` is ``INSTRUMENT_BORESIGHT``, - ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` or ``VECTOR_IN_INSTRUMENT_FOV``. + Warnings + -------- + Attributes :py:attr:`direction_instrument` is needed only if + :py:attr:`direction_vector_type` is ``INSTRUMENT_BORESIGHT``, + ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` or ``VECTOR_IN_INSTRUMENT_FOV``. - Attributes :py:attr:`direction_frame` is needed only if - :py:attr:`direction_vector_type` is ``REFERENCE_FRAME_AXIS`` or - ``VECTOR_IN_REFERENCE_FRAME``. + Attributes :py:attr:`direction_frame` is needed only if + :py:attr:`direction_vector_type` is ``REFERENCE_FRAME_AXIS`` or + ``VECTOR_IN_REFERENCE_FRAME``. - Attributes :py:attr:`direction_vector_x` + :py:attr:`direction_vector_y` + - :py:attr:`direction_vector_z` or :py:attr:`direction_vector_ra` + - :py:attr:`direction_vector_dec` is needed only if - :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or - ``VECTOR_IN_REFERENCE_FRAME``. + Attributes :py:attr:`direction_vector_x` + :py:attr:`direction_vector_y` + + :py:attr:`direction_vector_z` or :py:attr:`direction_vector_ra` + + :py:attr:`direction_vector_dec` is needed only if + :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``. Raises ------ @@ -1036,13 +1036,13 @@ class TimeConversion(Calculation): output_time_custom_format: str See: :py:attr:`output_time_custom_format` - Warnings - -------- - Attributes :py:attr:`output_sclk_id` is needed only if - :py:attr:`output_time_system` is ``SPACECRAFT_CLOCK``. + Warnings + -------- + Attributes :py:attr:`output_sclk_id` is needed only if + :py:attr:`output_time_system` is ``SPACECRAFT_CLOCK``. - Attributes :py:attr:`output_time_custom_format` is needed only if - :py:attr:`output_time_format` is ``CUSTOM``. + Attributes :py:attr:`output_time_custom_format` is needed only if + :py:attr:`output_time_format` is ``CUSTOM``. """ @@ -1111,17 +1111,17 @@ class GFCoordinateSearch(Calculation): reference_frame: str or int See: :py:attr:`reference_frame` - Warnings - -------- - Attributes :py:attr:`upper_limit` is needed only if - :py:attr:`relational_condition` is ``RANGE``. + Warnings + -------- + Attributes :py:attr:`upper_limit` is needed only if + :py:attr:`relational_condition` is ``RANGE``. - Attributes :py:attr:`adjustment_value` is needed only if - :py:attr:`relational_condition` is ``ABSMIN`` or ``ABSMAX``. + Attributes :py:attr:`adjustment_value` is needed only if + :py:attr:`relational_condition` is ``ABSMIN`` or ``ABSMAX``. - Attribute :py:attr:`reference_value` is needed only if - :py:attr:`relational_condition` is ``=``, ``<``, ``>`` or - ``RANGE``. + Attribute :py:attr:`reference_value` is needed only if + :py:attr:`relational_condition` is ``=``, ``<``, ``>`` or + ``RANGE``. Raises ------ From b29810c9a7505632b2ff2a8bcd03f83f18535684 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Wed, 26 Mar 2025 14:42:24 +0100 Subject: [PATCH 12/12] Add not implemented error on Geometry Finder missing endpoints --- docs/api.rst | 2 +- docs/calculation.rst | 55 +++++++++++++++---- docs/cli.rst | 11 ++++ examples/calculation.ipynb | 26 +++++++++ setup.py | 14 +++++ tests/test_gf_errors.py | 56 ++++++++++++++++++++ webgeocalc/__init__.py | 27 ++++++---- webgeocalc/calculation_types.py | 94 ++++++++++++++++++++++++++++++++- webgeocalc/cli.py | 61 ++++++++++++++++++++- webgeocalc/errors.py | 14 +++++ webgeocalc/vars.py | 20 +++---- 11 files changed, 346 insertions(+), 34 deletions(-) create mode 100644 tests/test_gf_errors.py diff --git a/docs/api.rst b/docs/api.rst index e269146..ef2a042 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -38,7 +38,7 @@ and can be retrieved directly as items: 'WGC2 -- a WebGeocalc Server with enabled API at NAIF, JPL' >>> API['version'] -'2.7.6' +'2.8.2' Request kernel sets diff --git a/docs/calculation.rst b/docs/calculation.rst index 727c14e..ef53ebe 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -18,16 +18,16 @@ For now only these geometry/time calculations are implemented: - :py:class:`TangentPoint` - :py:class:`OsculatingElements` - :py:class:`GFCoordinateSearch` -- ``GFAngularSeparationSearch`` (not implemented) -- ``GFDistanceSearch`` (not implemented) -- ``GFSubPointSearch`` (not implemented) -- ``GFOccultationSearch`` (not implemented) -- ``GFSurfaceInterceptPointSearch`` (not implemented) -- ``GFTargetInInstrumentFovSearch`` (not implemented) -- ``GFRayInFovSearch`` (not implemented) -- ``GFRangeRateSearch`` (not implemented) -- ``GFPhaseAngleSearch`` (not implemented) -- ``GFIlluminationAnglesSearch`` (not implemented) +- :py:class:`GFAngularSeparationSearch` (not implemented) +- :py:class:`GFDistanceSearch` (not implemented) +- :py:class:`GFSubPointSearch` (not implemented) +- :py:class:`GFOccultationSearch` (not implemented) +- :py:class:`GFSurfaceInterceptPointSearch` (not implemented) +- :py:class:`GFTargetInInstrumentFovSearch` (not implemented) +- :py:class:`GFRayInFovSearch` (not implemented) +- :py:class:`GFRangeRateSearch` (not implemented) +- :py:class:`GFPhaseAngleSearch` (not implemented) +- :py:class:`GFIlluminationAnglesSearch` (not implemented) - :py:class:`TimeConversion` Import generic WebGeoCalc calculation object: @@ -1109,3 +1109,38 @@ Find time intervals when a coordinate of an observer-target position vector sati - :py:attr:`~Calculation.interval_filtering`: ``NO_FILTERING`` .. autoclass:: GFCoordinateSearch + +Geometry Finder: other searches +------------------------------- + +At the moment, the following geometry finder search calculation are not implemented: + +- :py:class:`GFAngularSeparationSearch` +- :py:class:`GFDistanceSearch` +- :py:class:`GFSubPointSearch` +- :py:class:`GFOccultationSearch` +- :py:class:`GFSurfaceInterceptPointSearch` +- :py:class:`GFTargetInInstrumentFovSearch` +- :py:class:`GFRayInFovSearch` +- :py:class:`GFRangeRateSearch` +- :py:class:`GFPhaseAngleSearch` +- :py:class:`GFIlluminationAnglesSearch` + +.. note:: + + You might be able to run some of them with a regular :py:class:`Calculation` object but is not tested yet. + If you want to implement them, contributions are always welcome + (don't forget to submit a merge request on the `main project`_). + +.. _`main project`: https://github.com/seignovert/python-webgeocalc + +.. autoclass:: GFAngularSeparationSearch +.. autoclass:: GFDistanceSearch +.. autoclass:: GFSubPointSearch +.. autoclass:: GFOccultationSearch +.. autoclass:: GFSurfaceInterceptPointSearch +.. autoclass:: GFTargetInInstrumentFovSearch +.. autoclass:: GFRayInFovSearch +.. autoclass:: GFRangeRateSearch +.. autoclass:: GFPhaseAngleSearch +.. autoclass:: GFIlluminationAnglesSearch diff --git a/docs/cli.rst b/docs/cli.rst index 92ab5c3..8895e10 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -252,6 +252,17 @@ Here is the list of all the calculation entry points available on the CLI: - ``wgc-osculating-elements`` - ``wgc-time-conversion`` - ``wgc-gf-coordinate-search`` +- ``wgc-gf-angular-separation-search`` (not implemented) +- ``wgc-gf-distance-search`` (not implemented) +- ``wgc-gf-sub-point-search`` (not implemented) +- ``wgc-gf-occultation-search`` (not implemented) +- ``wgc-gf-surface-intercept-point-search`` (not implemented) +- ``wgc-gf-target-in-instrument-fov-search`` (not implemented) +- ``wgc-gf-ray-in-fov-search`` (not implemented) +- ``wgc-gf-range-rate-search`` (not implemented) +- ``wgc-gf-phase-angle-search`` (not implemented) +- ``wgc-gf-illumination-angles-search`` (not implemented) + .. hint:: diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index 62f4c1b..b832ffd 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -1006,6 +1006,32 @@ " reference_value = 0.25,\n", ").run()" ] + }, + { + "cell_type": "markdown", + "metadata": { + "vscode": { + "languageId": "sql" + } + }, + "source": [ + "## Other Geometry Finder Searches\n", + "\n", + "At the moment, the following geometry finder search calculation are not implemented:\n", + "- `GFAngularSeparationSearch`\n", + "- `GFDistanceSearch`\n", + "- `GFSubPointSearch`\n", + "- `GFOccultationSearch`\n", + "- `GFSurfaceInterceptPointSearch`\n", + "- `GFTargetInInstrumentFovSearch`\n", + "- `GFRayInFovSearch`\n", + "- `GFRangeRateSearch`\n", + "- `GFPhaseAngleSearch`\n", + "- `GFIlluminationAnglesSearch`\n", + "\n", + "You might be able to run some of them with a regular `Calculation` object but is not tested yet.\n", + "If you want to implement them, contributions are always welcome (don't forget to submit a merge request on the [main project](https://github.com/seignovert/python-webgeocalc))." + ] } ], "metadata": { diff --git a/setup.py b/setup.py index c786788..86f034c 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,20 @@ 'wgc-osculating-elements = webgeocalc.cli:cli_osculating_elements', 'wgc-time-conversion = webgeocalc.cli:cli_time_conversion', 'wgc-gf-coordinate-search = webgeocalc.cli:cli_gf_coordinate_search', + 'wgc-gf-angular-separation-search = ' + 'webgeocalc.cli:cli_gf_angular_separation_search', + 'wgc-gf-distance-search = webgeocalc.cli:cli_gf_distance_search', + 'wgc-gf-sub-point-search = webgeocalc.cli:cli_gf_sub_point_search', + 'wgc-gf-occultation-search = webgeocalc.cli:cli_gf_occultation_search', + 'wgc-gf-surface-intercept-point-search = ' + 'webgeocalc.cli:cli_gf_surface_intercept_point_search', + 'wgc-gf-target-in-instrument-fov-search = ' + 'webgeocalc.cli:cli_gf_target_in_instrument_fov_search', + 'wgc-gf-ray-in-fov-search = webgeocalc.cli:cli_gf_ray_in_fov_search', + 'wgc-gf-range-rate-search = webgeocalc.cli:cli_gf_range_rate_search', + 'wgc-gf-phase-angle-search = webgeocalc.cli:cli_gf_phase_angle_search', + 'wgc-gf-illumination-angles-search = ' + 'webgeocalc.cli:cli_gf_illumination_angles_search', ], }, ) diff --git a/tests/test_gf_errors.py b/tests/test_gf_errors.py new file mode 100644 index 0000000..81568cf --- /dev/null +++ b/tests/test_gf_errors.py @@ -0,0 +1,56 @@ +"""Test WGC geometry finder errors.""" + +from pytest import raises + +from webgeocalc import GFAngularSeparationSearch +from webgeocalc.cli import ( + cli_gf_angular_separation_search, + cli_gf_distance_search, + cli_gf_illumination_angles_search, + cli_gf_occultation_search, + cli_gf_phase_angle_search, + cli_gf_range_rate_search, + cli_gf_ray_in_fov_search, + cli_gf_sub_point_search, + cli_gf_surface_intercept_point_search, + cli_gf_target_in_instrument_fov_search, +) + + +def test_gf_not_implemented_errors(): + """Test geometry finder not implemented errors.""" + with raises(NotImplementedError): + GFAngularSeparationSearch() + + +def test_gf_cli_errors(): + """Test geometry finder searches errors in CLI.""" + with raises(NotImplementedError): + cli_gf_angular_separation_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_distance_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_sub_point_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_occultation_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_surface_intercept_point_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_target_in_instrument_fov_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_ray_in_fov_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_range_rate_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_phase_angle_search(['--kernels', '5']) + + with raises(NotImplementedError): + cli_gf_illumination_angles_search(['--kernels', '5']) diff --git a/webgeocalc/__init__.py b/webgeocalc/__init__.py index 2132abe..30bc62e 100644 --- a/webgeocalc/__init__.py +++ b/webgeocalc/__init__.py @@ -3,7 +3,12 @@ from .api import API, Api, ESA_API, JPL_API from .calculation import Calculation from .calculation_types import (AngularSeparation, AngularSize, FrameTransformation, - GFCoordinateSearch, IlluminationAngles, + GFAngularSeparationSearch, + GFCoordinateSearch, GFDistanceSearch, + GFIlluminationAnglesSearch, GFOccultationSearch, + GFPhaseAngleSearch, GFRangeRateSearch, GFRayInFovSearch, + GFSubPointSearch, GFSurfaceInterceptPointSearch, + GFTargetInInstrumentFovSearch, IlluminationAngles, OsculatingElements, PhaseAngle, PointingDirection, StateVector, SubObserverPoint, SubSolarPoint, SurfaceInterceptPoint, TangentPoint, TimeConversion) @@ -29,16 +34,16 @@ 'TangentPoint', 'OsculatingElements', 'GFCoordinateSearch', - # 'GFAngularSeparationSearch' - # 'GFDistanceSearch' - # 'GFSubPointSearch' - # 'GFOccultationSearch' - # 'GFSurfaceInterceptPointSearch' - # 'GFTargetInInstrumentFovSearch' - # 'GFRayInFovSearch' - # 'GFRangeRateSearch' - # 'GFPhaseAngleSearch' - # 'GFIlluminationAnglesSearch' + 'GFAngularSeparationSearch', + 'GFDistanceSearch', + 'GFSubPointSearch', + 'GFOccultationSearch', + 'GFSurfaceInterceptPointSearch', + 'GFTargetInInstrumentFovSearch', + 'GFRayInFovSearch', + 'GFRangeRateSearch', + 'GFPhaseAngleSearch', + 'GFIlluminationAnglesSearch', 'TimeConversion', '__version__', ] diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index 9079cd4..e96b78c 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -1,7 +1,7 @@ """Webgeocalc calculations types.""" from .calculation import Calculation -from .errors import CalculationInvalidAttr +from .errors import CalculationInvalidAttr, CalculationNotImplemented from .vars import VALID_PARAMETERS @@ -1149,3 +1149,95 @@ def __init__(self, output_duration_units='SECONDS', kwargs['interval_filtering'] = interval_filtering super().__init__(**kwargs) + + +# pylint: disable=abstract-method +class GFAngularSeparationSearch(CalculationNotImplemented): + """Angular separation search geometry finder. + + Find time intervals when the angle between two bodies, + as seen by an observer, satisfies a condition. + + """ + + +class GFDistanceSearch(CalculationNotImplemented): + """Distance search geometry finder. + + Find time intervals when the distance between a target + and observer satisfies a condition. + + """ + + +class GFSubPointSearch(CalculationNotImplemented): + """Sub-point search geometry finder. + + Find time intervals when a coordinate of the sub-observer point + on a target satisfies a condition. + + """ + + +class GFOccultationSearch(CalculationNotImplemented): + """Occultation search geometry finder. + + Find time intervals when an observer sees one target occulted by, + or in transit across, another. + + """ + + +class GFSurfaceInterceptPointSearch(CalculationNotImplemented): + """Surface intercept point search geometry finder. + + Find time intervals when a coordinate of a surface intercept vector + satisfies a condition. + + """ + + +class GFTargetInInstrumentFovSearch(CalculationNotImplemented): + """Target in instrument fov search geometry finder. + + Find time intervals when a target intersects the space bounded by + the field-of-view of an instrument. + + """ + + +class GFRayInFovSearch(CalculationNotImplemented): + """Ray in fov search geometry finder. + + Find time intervals when a specified ray is contained in the space bounded + by an instrument's field-of-view. + + """ + + +class GFRangeRateSearch(CalculationNotImplemented): + """Range rate search geometry finder. + + Find time intervals when the range rate between a target and observer + satisfies a condition. + + """ + + +class GFPhaseAngleSearch(CalculationNotImplemented): + """Phase angle search geometry finder. + + Find time intervals for which a specified constraint on the phase angle defined + by an illumination source, a target, and an observer body centers is met. + + """ + + +class GFIlluminationAnglesSearch(CalculationNotImplemented): + """Illumination angles search geometry finder. + + Find the time intervals, within a specified time window, when one of the illumination + angles โ€” phase, incidence or emission โ€” at the specified target body surface point + as seen from an observer satisfies a given constraint. + + """ diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index fda423d..712151e 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -5,7 +5,12 @@ from .api import Api, ESA_API, JPL_API from .calculation_types import (AngularSeparation, AngularSize, Calculation, - FrameTransformation, GFCoordinateSearch, + FrameTransformation, GFAngularSeparationSearch, + GFCoordinateSearch, GFDistanceSearch, + GFIlluminationAnglesSearch, GFOccultationSearch, + GFPhaseAngleSearch, GFRangeRateSearch, GFRayInFovSearch, + GFSubPointSearch, GFSurfaceInterceptPointSearch, + GFTargetInInstrumentFovSearch, IlluminationAngles, OsculatingElements, PhaseAngle, PointingDirection, StateVector, SubObserverPoint, SubSolarPoint, SurfaceInterceptPoint, TangentPoint, @@ -369,3 +374,57 @@ def cli_time_conversion(argv=None): def cli_gf_coordinate_search(argv=None): """Submit geometry finder coordinate search with the CLI.""" cli_calculation(argv, GFCoordinateSearch, desc='Position Event Finder') + + +def cli_gf_angular_separation_search(argv=None): + """Submit angular separation search geometry finder with the CLI.""" + cli_calculation(argv, GFAngularSeparationSearch, + desc='Angular separation search geometry finder') + + +def cli_gf_distance_search(argv=None): + """Submit distance search geometry finder with the CLI.""" + cli_calculation(argv, GFDistanceSearch, desc='Distance search geometry finder') + + +def cli_gf_sub_point_search(argv=None): + """Submit sub-point search geometry finder with the CLI.""" + cli_calculation(argv, GFSubPointSearch, desc='Sub-point search geometry finder') + + +def cli_gf_occultation_search(argv=None): + """Submit occultation search geometry finder with the CLI.""" + cli_calculation(argv, GFOccultationSearch, desc='Occultation search geometry finder') + + +def cli_gf_surface_intercept_point_search(argv=None): + """Submit surface intercept point search geometry finder with the CLI.""" + cli_calculation(argv, GFSurfaceInterceptPointSearch, + desc='Surface intercept point search geometry finder') + + +def cli_gf_target_in_instrument_fov_search(argv=None): + """Submit target in instrument fov search geometry finder with the CLI.""" + cli_calculation(argv, GFTargetInInstrumentFovSearch, + desc='Target in instrument fov search geometry finder') + + +def cli_gf_ray_in_fov_search(argv=None): + """Submit ray in fov search geometry finder with the CLI.""" + cli_calculation(argv, GFRayInFovSearch, desc='Ray in fov search geometry finder') + + +def cli_gf_range_rate_search(argv=None): + """Submit range rate search geometry finder with the CLI.""" + cli_calculation(argv, GFRangeRateSearch, desc='Range rate search geometry finder') + + +def cli_gf_phase_angle_search(argv=None): + """Submit phase angle search geometry finder with the CLI.""" + cli_calculation(argv, GFPhaseAngleSearch, desc='Phase angle search geometry finder') + + +def cli_gf_illumination_angles_search(argv=None): + """Submit illumination angles search geometry finder with the CLI.""" + cli_calculation(argv, GFIlluminationAnglesSearch, + desc='Illumination angles search geometry finder') diff --git a/webgeocalc/errors.py b/webgeocalc/errors.py index fdde819..c446526 100644 --- a/webgeocalc/errors.py +++ b/webgeocalc/errors.py @@ -136,3 +136,17 @@ def __init__(self, timeout, sleep): msg = f'Calculation time-out after {timeout} seconds' + \ f' ({int(timeout / sleep)} attempts)' super().__init__(msg) + + +class CalculationNotImplemented: + """Not implemented calculation. + + Only raise ``NotImplementedError``. + + """ + + def __init__(self, *_, **__): + raise NotImplementedError( + f'Calculation {self.__class__.__name__} is not yet implemented. ' + 'Feel free to implement it and submit a merge request.' + ) diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 4acd3d7..ce72ab3 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -25,16 +25,16 @@ 'TANGENT_POINT', 'OSCULATING_ELEMENTS', 'GF_COORDINATE_SEARCH', - # 'GF_ANGULAR_SEPARATION_SEARCH', - # 'GF_DISTANCE_SEARCH', - # 'GF_SUB_POINT_SEARCH', - # 'GF_OCCULTATION_SEARCH', - # 'GF_SURFACE_INTERCEPT_POINT_SEARCH', - # 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', - # 'GF_RAY_IN_FOV_SEARCH', - # 'GF_RANGE_RATE_SEARCH', - # 'GF_PHASE_ANGLE_SEARCH', - # 'GF_ILLUMINATION_ANGLES_SEARCH', + 'GF_ANGULAR_SEPARATION_SEARCH', + 'GF_DISTANCE_SEARCH', + 'GF_SUB_POINT_SEARCH', + 'GF_OCCULTATION_SEARCH', + 'GF_SURFACE_INTERCEPT_POINT_SEARCH', + 'GF_TARGET_IN_INSTRUMENT_FOV_SEARCH', + 'GF_RAY_IN_FOV_SEARCH', + 'GF_RANGE_RATE_SEARCH', + 'GF_PHASE_ANGLE_SEARCH', + 'GF_ILLUMINATION_ANGLES_SEARCH', 'TIME_CONVERSION', ], 'TIME_SYSTEM': [