From 78902f6e882a7a0ed98924a295705dfb0e71844f Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 11:42:53 +0100 Subject: [PATCH 01/25] Add `TWO_DIRECTIONS` implementation --- webgeocalc/calculation_types.py | 51 ++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index bbc8b97..aa113ac 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -67,12 +67,16 @@ class AngularSeparation(Calculation): """Angular separation calculation. Calculates the angular separation of two bodies as seen by an observer body. + There are two types of calculation. The default one is the angular separation + between two targets (`TWO_TARGETS` mode). The second case is the angular + separation between two directions (`TWO_DIRECTIONS` mode). + Parameters ---------- - shape_1: str, optional + shape_1: str, optional (`TWO_TARGETS` mode) See: :py:attr:`shape_1` - shape_2: str, optional + shape_2: str, optional (`TWO_TARGETS` mode) See: :py:attr:`shape_2` aberration_correction: str, optional See: :py:attr:`aberration_correction` @@ -95,29 +99,50 @@ class AngularSeparation(Calculation): See: :py:attr:`time_system` time_format: str See: :py:attr:`time_format` - target_1: str or int + spec_type: str, optional + See: :py:attr:`spec_type` + target_1: str or int (`TWO_TARGETS` mode) See: :py:attr:`target_1` - target_2: str or int + target_2: str or int (`TWO_TARGETS` mode) See: :py:attr:`target_2` - observer: str or int + observer: str or int (`TWO_TARGETS` mode) See: :py:attr:`observer` + direction_1: dict (`TWO_DIRECTIONS` mode) + See: :py:attr:`direction_1` + direction_2: dict (`TWO_DIRECTIONS` mode) + See: :py:attr:`direction_2` + Raises ------ CalculationRequiredAttr - If :py:attr:`target_1`, :py:attr:`target_2` and - :py:attr:`observer` are not provided. + If :py:attr:`spec_type` is `TWO_TARGETS` or not set: + If py:attr:`target_1`, :py:attr:`target_2` + and :py:attr:`observer` are not provided. + If :py:attr:`spec_type` is `TWO_DIRECTIONS`: + If :py:attr:`direction_1` and :py:attr:`direction_2` + are not provided. """ - REQUIRED = ('target_1', 'target_2', 'observer') - - def __init__(self, shape_1='POINT', shape_2='POINT', + def __init__(self, spec_type='TWO_TARGETS', aberration_correction='CN', **kwargs): kwargs['calculation_type'] = 'ANGULAR_SEPARATION' - kwargs['shape_1'] = shape_1 - kwargs['shape_2'] = shape_2 + + spec_type_required = { + 'TWO_TARGETS': ('target_1', 'target_2', 'observer'), + 'TWO_DIRECTIONS': ('direction_1', 'direction_2') + } + self.REQUIRED = spec_type_required[spec_type] + + if spec_type == 'TWO_TARGETS': + for key in ['shape_1', 'shape_2']: + kwargs.setdefault(key, 'POINT') + + if spec_type == 'TWO_DIRECTIONS': + kwargs['spec_type'] = spec_type + kwargs['aberration_correction'] = aberration_correction super().__init__(**kwargs) @@ -174,7 +199,7 @@ def __init__(self, aberration_correction='CN', **kwargs): class FrameTransformation(Calculation): - """Frame transforme calculation. + """Frame transform calculation. Calculate the transformation from one reference frame (Frame 1) to another reference frame (Frame 2). From 3aacfaf6d4a6481f44c272fa5374571394567893 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 14:09:39 +0100 Subject: [PATCH 02/25] Add `TWO_DIRECTIONS` angular separation calculation (and test) --- tests/test_angular_separation.py | 75 ++++++++++++++++++++++++++++++++ webgeocalc/calculation.py | 54 +++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 71650e8..79804f1 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -14,6 +14,12 @@ def kernel_paths(): ] +@fixture +def kernels(): + """Input kernel set.""" + return 5 + + @fixture def time(): """Input time.""" @@ -83,6 +89,75 @@ def payload(kernel_paths, time, target_1, target_2, observer, corr): } +@fixture +def kernel_set(): + """Input kernel set.""" + return 5 + +@fixture +def direction_1(): + """Input first direction.""" + return { + "directionType": "VECTOR", + "directionVectorType": "REFERENCE_FRAME_AXIS", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionFrameAxis": "Z" + } + + +@fixture +def direction_2(): + """Input second direction.""" + return { + "directionType": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + } + + +@fixture +def params_two_directions(kernel_set, time, direction_1, direction_2, corr): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_1, + 'direction_2': direction_2, + 'aberration_correction': corr + } + +@fixture +def payload_two_directions(kernel_set, time, direction_1, direction_2, corr): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": 5, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_1, + "direction2": direction_2, + "aberrationCorrection": corr + } + + + def test_angular_separation_payload(params, payload): """Test angular separation payload.""" assert AngularSeparation(**params).payload == payload + + +def test_angular_separation_payload_two_directions( + params_two_directions, + payload_two_directions +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions).payload == payload_two_directions diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 7a27075..eaa4699 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -2039,3 +2039,57 @@ def gf_condition(self, **kwargs): ) from None self.__condition = kwargs + + @parameter + def spec_type(self, val): + """Type of angular separation calculation. + + Parameters + ---------- + spec_type: str + One of the following: + + - TWO_TARGETS + - TWO_DIRECTIONS + """ + self.__specType = val + + @parameter + def spec_type(self, val): + """The specification type for anguler separation. + + Parameters + ---------- + spec_type: str + Specification type. + """ + + self.__specType = val + + @parameter + def direction_1(self, val): + """The first direction definition for two-directions angular separation. + + Parameters + ---------- + direction_1: dict + Direction configuration. See: :py:func:`direction`. + + """ + val.setdefault("aberrationCorrection", "NONE") + val.setdefault("antiVectorFlag", False) + self.__direction1 = val + + @parameter + def direction_2(self, val): + """The second direction definition for two-directions angular separation. + + Parameters + ---------- + direction_2: dict + Direction configuration. See: :py:func:`direction`. + + """ + val.setdefault("aberrationCorrection", "NONE") + val.setdefault("antiVectorFlag", False) + self.__direction2 = val From bc61a307e355ba992aed92a0d93c1d3566939c83 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 14:11:58 +0100 Subject: [PATCH 03/25] Update for Pylint anf flake8 compliance --- tests/test_angular_separation.py | 5 +++-- webgeocalc/calculation.py | 12 ------------ 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 79804f1..3f51da4 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -94,6 +94,7 @@ def kernel_set(): """Input kernel set.""" return 5 + @fixture def direction_1(): """Input first direction.""" @@ -128,13 +129,14 @@ def params_two_directions(kernel_set, time, direction_1, direction_2, corr): 'aberration_correction': corr } + @fixture def payload_two_directions(kernel_set, time, direction_1, direction_2, corr): """Input parameters from WGC API example.""" return { "kernels": [{ "type": "KERNEL_SET", - "id": 5, + "id": kernel_set, }], "specType": "TWO_DIRECTIONS", "timeSystem": "UTC", @@ -149,7 +151,6 @@ def payload_two_directions(kernel_set, time, direction_1, direction_2, corr): } - def test_angular_separation_payload(params, payload): """Test angular separation payload.""" assert AngularSeparation(**params).payload == payload diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index eaa4699..e9491c0 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -2054,18 +2054,6 @@ def spec_type(self, val): """ self.__specType = val - @parameter - def spec_type(self, val): - """The specification type for anguler separation. - - Parameters - ---------- - spec_type: str - Specification type. - """ - - self.__specType = val - @parameter def direction_1(self, val): """The first direction definition for two-directions angular separation. From 6b21e32793f28cc96a38d11ae8f9c800109a4905 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 14:25:58 +0100 Subject: [PATCH 04/25] Add example of `TWO_DIRECTIONS` angular separation calculation in docs --- docs/calculation.rst | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/calculation.rst b/docs/calculation.rst index 056b6de..65379d7 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -339,7 +339,9 @@ Angular Separation ------------------ Calculates the angular separation of two bodies as seen by an -observer body. +observer body. There are two types of calculation. The default +one is the angular separation between two targets (`TWO_TARGETS` +mode), which is the default mode. >>> AngularSeparation( ... kernel_paths = ['pds/wgc/kernels/lsk/naif0012.tls', @@ -352,14 +354,39 @@ observer body. ... ).run() {'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 175.17072258} +The second case is the angular separation between two directions +(`TWO_DIRECTIONS` mode). + +>>> AngularSeparation( +... spec_type = 'TWO_DIRECTIONS', +... kernels = 5, +... times = '2012-10-19T08:24:00.000', +... direction_1 = { +... "directionType": "VECTOR", +... "directionVectorType": "REFERENCE_FRAME_AXIS", +... "directionFrame": "CASSINI_RPWS_EDIPOLE", +... "directionFrameAxis": "Z" +... }, +... direction_2 = { +... "directionType": "POSITION", +... "target": "SUN", +... "shape": "POINT", +... "observer": "CASSINI" +... }, +... verbose = False, +... ).run() +{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 90.10114616} + .. important:: Calculation required parameters: - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target_1` - - :py:attr:`.target_2` - - :py:attr:`.observer` + - :py:attr:`.target_1` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`.target_2` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`.observer` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`.direction_1` if :py:attr:`.spec_type` is `TWO_DIRECTIONS` + - :py:attr:`.direction_2` if :py:attr:`.spec_type` is `TWO_DIRECTIONS` Default parameters: - :py:attr:`.time_system`: ``UTC`` From b7209e73d8da068345691e9b09780d261044b495 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 14:32:08 +0100 Subject: [PATCH 05/25] Add example of `TWO_DIRECTIONS` angular separation calculation in jupyter notebook example --- examples/calculation.ipynb | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index e9635e2..97182f4 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -307,6 +307,64 @@ ").run()" ] }, + { + "cell_type": "markdown", + "source": [ + "Calculates the angular separation of two directions: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#angular-separation)) " + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Calculation submit] Phase: COMPLETE (id: 6d697dad-dd2b-40df-af20-bd2d97a96fbc)\n" + ] + }, + { + "data": { + "text/plain": "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 90.10114616}" + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from webgeocalc import AngularSeparation\n", + "\n", + "AngularSeparation(\n", + " spec_type = 'TWO_DIRECTIONS',\n", + " kernels = 5,\n", + " times = '2012-10-19T08:24:00.000',\n", + " direction_1 = {\n", + " \"directionType\": \"VECTOR\",\n", + " \"directionVectorType\": \"REFERENCE_FRAME_AXIS\",\n", + " \"directionFrame\": \"CASSINI_RPWS_EDIPOLE\",\n", + " \"directionFrameAxis\": \"Z\"\n", + " },\n", + " direction_2 = {\n", + " \"directionType\": \"POSITION\",\n", + " \"target\": \"SUN\",\n", + " \"shape\": \"POINT\",\n", + " \"observer\": \"CASSINI\"\n", + " },\n", + ").run()" + ], + "metadata": { + "collapsed": false, + "ExecuteTime": { + "end_time": "2025-03-04T13:28:44.424317Z", + "start_time": "2025-03-04T13:28:42.241069Z" + } + }, + "execution_count": 1 + }, { "cell_type": "markdown", "metadata": {}, From e5660489ce8da92091ec7f087faf48f2b09e0b9b Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Tue, 4 Mar 2025 14:59:02 +0100 Subject: [PATCH 06/25] Update docstring --- webgeocalc/calculation.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index e9491c0..4af72e2 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -2056,13 +2056,15 @@ def spec_type(self, val): @parameter def direction_1(self, val): - """The first direction definition for two-directions angular separation. + """The first direction object. + + Definition of first direction for two-directions angular + separation calculation. Parameters ---------- direction_1: dict Direction configuration. See: :py:func:`direction`. - """ val.setdefault("aberrationCorrection", "NONE") val.setdefault("antiVectorFlag", False) @@ -2070,13 +2072,15 @@ def direction_1(self, val): @parameter def direction_2(self, val): - """The second direction definition for two-directions angular separation. + """The second direction object. + + Definition of second direction for two-directions angular + separation calculation. Parameters ---------- direction_2: dict Direction configuration. See: :py:func:`direction`. - """ val.setdefault("aberrationCorrection", "NONE") val.setdefault("antiVectorFlag", False) From 6162a5ee07147ecdf42ba6d497ad1cadc6465b98 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Wed, 5 Mar 2025 12:36:57 +0100 Subject: [PATCH 07/25] Add new VALID_PARAMETERS --- webgeocalc/vars.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 677d646..3a820f6 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -97,6 +97,15 @@ 'CN', 'CN+S', ], + 'ABERRATION_CORRECTION_VECTOR': [ + 'NONE', + 'LT', + 'CN', + 'XLT', + 'XCN', + 'S', + 'XS', + ], 'STATE_REPRESENTATION': [ 'RECTANGULAR', 'RA_DEC', @@ -206,5 +215,18 @@ 'ABSMIN', 'LOCMAX', 'LOCMIN', - ] + ], + "SPEC_TYPE": [ + "TWO_TARGETS", + "TWO_DIRECTIONS", + ], + "TARGET_SHAPE": [ + "POINT", + "SPHERE", + ], + "DIRECTION_TYPE": [ + "POSITION", + "VELOCITY", + "VECTOR", + ], } From b5ac424874a261264a05521a569384229ce61148 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Wed, 5 Mar 2025 12:37:30 +0100 Subject: [PATCH 08/25] Add new direction object handling --- webgeocalc/calculation.py | 435 +++++++++++++++++++++++++++++++++++++- 1 file changed, 428 insertions(+), 7 deletions(-) diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 4af72e2..bd28d00 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -1939,6 +1939,7 @@ def relational_condition(self, val): is not supplied. If the value is ``=``, ``<``, ``>`` or ``RANGE``, and :py:attr:`reference_value` is not supplied. + """ self.gf_condition(relationalCondition=val) @@ -2013,6 +2014,7 @@ def gf_condition(self, **kwargs): If :py:attr:`calculation_type` is ``GF_COORDINATE_SEARCH``, ``GF_SUB_POINT_SEARCH`` or ``GF_SURFACE_INTERCEPT_POINT_SEARCH``, and :py:attr:`coordinate_system` or :py:attr:`coordinate` are not present. + """ try: self.__condition.update(kwargs) @@ -2051,6 +2053,7 @@ def spec_type(self, val): - TWO_TARGETS - TWO_DIRECTIONS + """ self.__specType = val @@ -2065,10 +2068,9 @@ def direction_1(self, val): ---------- direction_1: dict Direction configuration. See: :py:func:`direction`. + """ - val.setdefault("aberrationCorrection", "NONE") - val.setdefault("antiVectorFlag", False) - self.__direction1 = val + self.__direction1 = _Direction(**val).payload @parameter def direction_2(self, val): @@ -2080,8 +2082,427 @@ def direction_2(self, val): Parameters ---------- direction_2: dict - Direction configuration. See: :py:func:`direction`. + Direction configuration. See: :py:class:`_Direction`. + + """ + self.__direction2 = _Direction(**val).payload + + +class _Direction: + """Webgeocalc direction object.""" + + def __init__(self, aberration_correction="NONE", + anti_vector_flag=False, **kwargs): + + if kwargs["direction_type"] == "VECTOR": + kwargs["aberration_correction_vector"] = aberration_correction + else: + kwargs['aberration_correction'] = aberration_correction + + kwargs['anti_vector_flag'] = anti_vector_flag + + for key, value in kwargs.items(): + setattr(self, key, value) + + @property + def payload(self): + """Direction payload parameters *dict* for JSON input in WebGeoCalc format. + + Return + ------ + dict + Payload keys and values. + + """ + return {k.split('__')[-1]: v for k, v in vars(self).items() if k.startswith('_')} + + @parameter(only='ABERRATION_CORRECTION') + def aberration_correction(self, val): + """SPICE aberration correction. + + Parameters + ---------- + aberration_correction: str + The SPICE aberration correction string. One of: + + - NONE + - LT + - LT+S + - CN + - CN+S + - XLT + - XLT+S + - XCN + - XCN+S + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__aberrationCorrection = val + + @parameter(only='DIRECTION_TYPE') + def direction_type(self, val): + """Type of direction. + + Method used to specify a direction. Directions could be specified as the + position of an object as seen from the observer, as the velocity vector of + an object as seen from the observer in a given reference frame, or by + providing a vector in a given reference frame. + + Parameters + ---------- + direction_type: str + The type of direction string. One of: + + - POSITION + - VELOCITY + - VECTOR + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__directionType = val + + @parameter + def anti_vector_flag(self, val): + """Anti Vector Flag. + + Parameters + ---------- + anti_vector_flag: bool + `True` if the anti-vector shall be used for the direction, and `False` + otherwise. Required when the target shape is `POINT`. If provided when + the target shape is `SPHERE`, it must be set to false, i.e., using + anti-vector direction is not supported for target bodies modeled as + spheres. + + """ + if not isinstance(val, bool): + raise CalculationInvalidAttr( + name="ANTI_VECTOR_FLAG", + attr=val, + valids=["True", "False"] + ) + self.__antiVectorFlag = val + + @parameter + def target(self, val): + """Target body. + + Parameters + ---------- + target: str or int + The target body ``name`` or ``id`` from :py:func:`API.bodies`. + + """ + self.__target = val if isinstance(val, int) else val.upper() + + @parameter(only='TARGET_SHAPE') + def shape(self, val): + """The shape to use for the first body. + + Parameters + ---------- + shape: str + One of: + - POINT + - SPHERE + + Raises + ------- + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__shape = val + + @parameter + def observer(self, val): + """Observing body. + + Parameters + ---------- + observer: str or int + The observing body ``name`` or ``id`` from :py:func:`API.bodies`. + + """ + self.__observer = val if isinstance(val, int) else val.upper() + + @parameter + def reference_frame(self, val): + """The reference frame name. + + Parameters + ---------- + reference_frame: str + The reference frame name. + + """ + self.__referenceFrame = val.upper() + + @parameter(only='ABERRATION_CORRECTION_VECTOR') + def aberration_correction_vector(self, val): + """SPICE aberration correction. + + Parameters + ---------- + aberration_correction: str + The SPICE aberration correction string. One of: + + - NONE + - LT + - CN + - XLT + - XCN + - S + - XS + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__aberrationCorrection = val + + @parameter(only='DIRECTION_VECTOR_TYPE') + def direction_vector_type(self, val): + """Direction vector type. + + Parameters + ---------- + direction_vector_type: str + The direction vector type string. One of: + + - INSTRUMENT_BORESIGHT + - INSTRUMENT_FOV_BOUNDARY_VECTORS + - REFERENCE_FRAME_AXIS + - VECTOR_IN_INSTRUMENT_FOV + - VECTOR_IN_REFERENCE_FRAME + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__directionVectorType = val + + @parameter + def direction_instrument(self, val): + """The instrument name or ID. + + Required only if directionVectorType is INSTRUMENT_BORESIGHT, + VECTOR_IN_INSTRUMENT_FOV or INSTRUMENT_FOV_BOUNDARY_VECTORS. + + Parameters + ---------- + direction_instrument: str or int + The instrument ``name`` or ``ID``. + + """ + self.__directionInstrument = val if isinstance(val, int) else val.upper() + + @parameter + def direction_frame(self, val): + """The vector's reference frame name. + + Required only if directionVectorType is REFERENCE_FRAME_AXIS + or VECTOR_IN_REFERENCE_FRAME. + + Parameters: + ----------- + direction_frame: str + The vector's reference frame name. + + """ + self.__directionFrame = val + + @parameter(only='AXIS') + def direction_frame_axis(self, val): + """The direction vector frame axis. + + Required only if directionVectorType is REFERENCE_FRAME_AXIS. + + Parameters + ---------- + direction_frame_axis: str + The direction frame axis string. One of: + + - X + - Y + - Z + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__directionFrameAxis = val + + @parameter + def direction_vector_x(self, val): + """The X direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_x: float + The X direction vector coordinate value. + + """ + self.__directionVectorX = val + + @parameter + def direction_vector_y(self, val): + """The Y direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_y: float + The Y direction vector coordinate value. + + """ + self.__directionVectorY = val + + @parameter + def direction_vector_z(self, val): + """The Z direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_z: float + The Z direction vector coordinate value. + + """ + self.__directionVectorZ = val + + @parameter + def direction_vector_ra(self, val): + """The right ascension direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_ra: float + The right ascension direction vector coordinate value. + + """ + self.__directionVectorRA = val + + @parameter + def direction_vector_dec(self, val): + """The declination direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_dec: float + The declination direction vector coordinate value. + + """ + self.__directionVectorDec = val + + @parameter + def direction_vector_az(self, val): + """The azimuth direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_az: float + The azimuth direction vector coordinate value. + + """ + self.__directionVectorAz = val + + @parameter + def direction_vector_el(self, val): + """The elevation direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_el: float + The elevation vector coordinate value. + + """ + self.__directionVectorEl = val + + @parameter + def azccw_flag(self, val): + """Flag indicating how azimuth is measured. + + If azccwFlag is ``true``, azimuth increases in the counterclockwise + direction; otherwise it increases in the clockwise direction. Required + only when directionVectorAz and directionVectorEl are used to provide + the coordinates of the direction vector. + + Parameter + --------- + azccw_flag: bool + Flag indicating how azimuth is measured. + + """ + self.__azccwFlag = val + + @parameter + def elplsz_flag(self, val): + """Flag indicating how elevation is measured. + + If elplszFlag is true, elevation increases from the XY plane toward + +Z; otherwise toward -Z. Required only when directionVectorAz and + directionVectorEl are used to provide the coordinates of the direction + vector. + + Parameter + --------- + elplsz_flag: bool + ag indicating how elevation is measured. + """ - val.setdefault("aberrationCorrection", "NONE") - val.setdefault("antiVectorFlag", False) - self.__direction2 = val + self.__elplszFlag = val From 903cc8c1efaa4dea8a385c59d24f05d3d08a7d39 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Wed, 5 Mar 2025 12:38:06 +0100 Subject: [PATCH 09/25] Add tests and docs for new direction handling in AngularSeparation calculation --- docs/calculation.rst | 10 +++++----- examples/calculation.ipynb | 10 +++++----- tests/test_angular_separation.py | 33 +++++++++++++++++++++++++++++--- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/docs/calculation.rst b/docs/calculation.rst index 65379d7..f87eb6b 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -362,13 +362,13 @@ The second case is the angular separation between two directions ... kernels = 5, ... times = '2012-10-19T08:24:00.000', ... direction_1 = { -... "directionType": "VECTOR", -... "directionVectorType": "REFERENCE_FRAME_AXIS", -... "directionFrame": "CASSINI_RPWS_EDIPOLE", -... "directionFrameAxis": "Z" +... "direction_type": "VECTOR", +... "direction_vector_type": "REFERENCE_FRAME_AXIS", +... "direction_frame": "CASSINI_RPWS_EDIPOLE", +... "direction_frame_axis": "Z" ... }, ... direction_2 = { -... "directionType": "POSITION", +... "direction_type": "POSITION", ... "target": "SUN", ... "shape": "POINT", ... "observer": "CASSINI" diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index 97182f4..d16c645 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -343,13 +343,13 @@ " kernels = 5,\n", " times = '2012-10-19T08:24:00.000',\n", " direction_1 = {\n", - " \"directionType\": \"VECTOR\",\n", - " \"directionVectorType\": \"REFERENCE_FRAME_AXIS\",\n", - " \"directionFrame\": \"CASSINI_RPWS_EDIPOLE\",\n", - " \"directionFrameAxis\": \"Z\"\n", + " \"direction_type\": \"VECTOR\",\n", + " \"direction_vector_type\": \"REFERENCE_FRAME_AXIS\",\n", + " \"direction_frame\": \"CASSINI_RPWS_EDIPOLE\",\n", + " \"direction_frame_axis\": \"Z\"\n", " },\n", " direction_2 = {\n", - " \"directionType\": \"POSITION\",\n", + " \"direction_type\": \"POSITION\",\n", " \"target\": \"SUN\",\n", " \"shape\": \"POINT\",\n", " \"observer\": \"CASSINI\"\n", diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 3f51da4..1645275 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -99,6 +99,19 @@ def kernel_set(): def direction_1(): """Input first direction.""" return { + "direction_type": "VECTOR", + "direction_vector_type": "REFERENCE_FRAME_AXIS", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_frame_axis": "Z" + } + + +@fixture +def direction1_payload(): + """First direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, "directionType": "VECTOR", "directionVectorType": "REFERENCE_FRAME_AXIS", "directionFrame": "CASSINI_RPWS_EDIPOLE", @@ -110,6 +123,19 @@ def direction_1(): def direction_2(): """Input second direction.""" return { + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + } + + +@fixture +def direction2_payload(): + """Input second direction.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, "directionType": "POSITION", "target": "SUN", "shape": "POINT", @@ -131,7 +157,8 @@ def params_two_directions(kernel_set, time, direction_1, direction_2, corr): @fixture -def payload_two_directions(kernel_set, time, direction_1, direction_2, corr): +def payload_two_directions(kernel_set, time, direction1_payload, direction2_payload, + corr): """Input parameters from WGC API example.""" return { "kernels": [{ @@ -145,8 +172,8 @@ def payload_two_directions(kernel_set, time, direction_1, direction_2, corr): time, ], "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_1, - "direction2": direction_2, + "direction1": direction1_payload, + "direction2": direction2_payload, "aberrationCorrection": corr } From b91c8fddc4c21dd3db9fe2e4d602d770e5e66f06 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Wed, 5 Mar 2025 14:03:42 +0100 Subject: [PATCH 10/25] Add tests for new direction class --- tests/test_angular_separation.py | 461 +++++++++++++++++++++++++++++-- 1 file changed, 435 insertions(+), 26 deletions(-) diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 1645275..85ed42f 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -1,7 +1,9 @@ """Test WGC angular separation calculation.""" from pytest import fixture +from pytest import raises +from webgeocalc.errors import CalculationInvalidAttr from webgeocalc import AngularSeparation @@ -14,12 +16,6 @@ def kernel_paths(): ] -@fixture -def kernels(): - """Input kernel set.""" - return 5 - - @fixture def time(): """Input time.""" @@ -96,8 +92,30 @@ def kernel_set(): @fixture -def direction_1(): - """Input first direction.""" +def direction_vector_inst_boresight(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "INSTRUMENT_BORESIGHT", + "direction_instrument": "CASSINI_ISS_NAC", + } + + +@fixture +def direction_vector_inst_boresight_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "INSTRUMENT_BORESIGHT", + "directionInstrument": "CASSINI_ISS_NAC", + } + + +@fixture +def direction_vector_ref_frame_axis(): + """Input vector direction.""" return { "direction_type": "VECTOR", "direction_vector_type": "REFERENCE_FRAME_AXIS", @@ -107,8 +125,8 @@ def direction_1(): @fixture -def direction1_payload(): - """First direction payload.""" +def direction_vector_ref_frame_axis_payload(): + """Vector direction payload.""" return { 'aberrationCorrection': 'NONE', 'antiVectorFlag': False, @@ -120,8 +138,118 @@ def direction1_payload(): @fixture -def direction_2(): - """Input second direction.""" +def direction_vector_in_ref_frame_xyz(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_x": 0.0, + "direction_vector_y": 0.0, + "direction_vector_z": 1.0, + } + + +@fixture +def direction_vector_in_ref_frame_xyz_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorX": 0.0, + "directionVectorY": 0.0, + "directionVectorZ": 1.0, + } + + +@fixture +def direction_vector_in_ref_frame_radec(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_ra": 0.0, + "direction_vector_dec": 0.0, + } + + +@fixture +def direction_vector_in_ref_frame_radec_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorRA": 0.0, + "directionVectorDec": 0.0, + } + + + + +@fixture +def direction_vector_in_ref_frame_azel(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_az": 0.0, + "direction_vector_el": 0.0, + "azccw_flag": True, + "elplsz_flag": True, + } + + +@fixture +def direction_vector_in_ref_frame_azel_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorAz": 0.0, + "directionVectorEl": 0.0, + "azccwFlag": True, + "elplszFlag": True, + } + + +@fixture +def direction_velocity(): + """Input velocity direction.""" + return { + "direction_type": "VELOCITY", + "target": "CASSINI", + "reference_frame": "IAU_SATURN", + "observer": "SATURN" + } + + +@fixture +def direction_velocity_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VELOCITY", + "target": "CASSINI", + "referenceFrame": "IAU_SATURN", + "observer": "SATURN" + } + + +@fixture +def direction_position(): + """Input position direction.""" return { "direction_type": "POSITION", "target": "SUN", @@ -129,10 +257,9 @@ def direction_2(): "observer": "CASSINI" } - @fixture -def direction2_payload(): - """Input second direction.""" +def direction_position_payload(): + """Position direction payload.""" return { 'aberrationCorrection': 'NONE', 'antiVectorFlag': False, @@ -144,21 +271,93 @@ def direction2_payload(): @fixture -def params_two_directions(kernel_set, time, direction_1, direction_2, corr): +def direction_position_anti_vector_flag_invalid(): + """Input position direction.""" + return { + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI", + "anti_vector_flag": "Test" + } + +@fixture +def params_two_directions_anti_vector_flag_invalid( + kernel_set, time, direction_position_anti_vector_flag_invalid, + direction_position, corr +): + """Input parameters for with invalid anti-vector-flag.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_position_anti_vector_flag_invalid, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def params_two_directions_1( + kernel_set, time, direction_vector_ref_frame_axis, + direction_position, corr +): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector_ref_frame_axis, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def payload_two_directions_1( + kernel_set, time, direction_vector_ref_frame_axis_payload, + direction_position_payload, corr +): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_ref_frame_axis_payload, + "direction2": direction_position_payload, + "aberrationCorrection": corr + } + + +@fixture +def params_two_directions_2( + kernel_set, time, direction_velocity, + direction_position, corr +): """Input parameters from WGC API example.""" return { 'spec_type': 'TWO_DIRECTIONS', 'kernels': kernel_set, 'times': time, - 'direction_1': direction_1, - 'direction_2': direction_2, + 'direction_1': direction_velocity, + 'direction_2': direction_position, 'aberration_correction': corr } @fixture -def payload_two_directions(kernel_set, time, direction1_payload, direction2_payload, - corr): +def payload_two_directions_2( + kernel_set, time, direction_velocity_payload, + direction_position_payload, corr +): """Input parameters from WGC API example.""" return { "kernels": [{ @@ -172,8 +371,171 @@ def payload_two_directions(kernel_set, time, direction1_payload, direction2_payl time, ], "calculationType": "ANGULAR_SEPARATION", - "direction1": direction1_payload, - "direction2": direction2_payload, + "direction1": direction_velocity_payload, + "direction2": direction_position_payload, + "aberrationCorrection": corr + } + + +@fixture +def params_two_directions_3( + kernel_set, time, direction_vector_inst_boresight, + direction_position, corr +): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector_inst_boresight, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def payload_two_directions_3( + kernel_set, time, direction_vector_inst_boresight_payload, + direction_position_payload, corr +): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_inst_boresight_payload, + "direction2": direction_position_payload, + "aberrationCorrection": corr + } + + +@fixture +def params_two_directions_4( + kernel_set, time, + direction_vector_in_ref_frame_xyz, + direction_position, corr +): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector_in_ref_frame_xyz, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def payload_two_directions_4( + kernel_set, time, direction_vector_in_ref_frame_xyz_payload, + direction_position_payload, corr +): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_in_ref_frame_xyz_payload, + "direction2": direction_position_payload, + "aberrationCorrection": corr + } + + +@fixture +def params_two_directions_5( + kernel_set, time, + direction_vector_in_ref_frame_radec, + direction_position, corr +): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector_in_ref_frame_radec, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def payload_two_directions_5( + kernel_set, time, direction_vector_in_ref_frame_radec_payload, + direction_position_payload, corr +): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_in_ref_frame_radec_payload, + "direction2": direction_position_payload, + "aberrationCorrection": corr + } + + +@fixture +def params_two_directions_6( + kernel_set, time, + direction_vector_in_ref_frame_azel, + direction_position, corr +): + """Input parameters from WGC API example.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector_in_ref_frame_azel, + 'direction_2': direction_position, + 'aberration_correction': corr + } + + +@fixture +def payload_two_directions_6( + kernel_set, time, direction_vector_in_ref_frame_azel_payload, + direction_position_payload, corr +): + """Input parameters from WGC API example.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [ + time, + ], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_in_ref_frame_azel_payload, + "direction2": direction_position_payload, "aberrationCorrection": corr } @@ -183,9 +545,56 @@ def test_angular_separation_payload(params, payload): assert AngularSeparation(**params).payload == payload -def test_angular_separation_payload_two_directions( - params_two_directions, - payload_two_directions +def test_angular_separation_payload_two_directions_1( + params_two_directions_1, + payload_two_directions_1 +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions_1).payload == payload_two_directions_1 + + +def test_angular_separation_payload_two_directions_1_anti_vector_flag_error( + params_two_directions_anti_vector_flag_invalid +): + """Test anti-vector flag error.""" + with raises(CalculationInvalidAttr): + AngularSeparation(**params_two_directions_anti_vector_flag_invalid) + +def test_angular_separation_payload_two_directions_2( + params_two_directions_2, + payload_two_directions_2 +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions_2).payload == payload_two_directions_2 + + +def test_angular_separation_payload_two_directions_3( + params_two_directions_3, + payload_two_directions_3 +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions_3).payload == payload_two_directions_3 + + +def test_angular_separation_payload_two_directions_4( + params_two_directions_4, + payload_two_directions_4 +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions_4).payload == payload_two_directions_4 + + +def test_angular_separation_payload_two_directions_5( + params_two_directions_5, + payload_two_directions_5 +): + """Test angular separation payload (`TWO_DIRECTIONS` mode).""" + assert AngularSeparation(**params_two_directions_5).payload == payload_two_directions_5 + + +def test_angular_separation_payload_two_directions_6( + params_two_directions_6, + payload_two_directions_6 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions).payload == payload_two_directions + assert AngularSeparation(**params_two_directions_6).payload == payload_two_directions_6 From 8ae0f0518f684b7120896176d641728b13aed621 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Wed, 5 Mar 2025 14:09:58 +0100 Subject: [PATCH 11/25] Refactor for I100, E303, E302, E501 --- tests/test_angular_separation.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 85ed42f..54e9e2f 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -3,8 +3,8 @@ from pytest import fixture from pytest import raises -from webgeocalc.errors import CalculationInvalidAttr from webgeocalc import AngularSeparation +from webgeocalc.errors import CalculationInvalidAttr @fixture @@ -191,8 +191,6 @@ def direction_vector_in_ref_frame_radec_payload(): } - - @fixture def direction_vector_in_ref_frame_azel(): """Input vector direction.""" @@ -257,6 +255,7 @@ def direction_position(): "observer": "CASSINI" } + @fixture def direction_position_payload(): """Position direction payload.""" @@ -281,6 +280,7 @@ def direction_position_anti_vector_flag_invalid(): "anti_vector_flag": "Test" } + @fixture def params_two_directions_anti_vector_flag_invalid( kernel_set, time, direction_position_anti_vector_flag_invalid, @@ -550,7 +550,8 @@ def test_angular_separation_payload_two_directions_1( payload_two_directions_1 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_1).payload == payload_two_directions_1 + payload = AngularSeparation(**params_two_directions_1).payload + assert payload == payload_two_directions_1 def test_angular_separation_payload_two_directions_1_anti_vector_flag_error( @@ -560,12 +561,14 @@ def test_angular_separation_payload_two_directions_1_anti_vector_flag_error( with raises(CalculationInvalidAttr): AngularSeparation(**params_two_directions_anti_vector_flag_invalid) + def test_angular_separation_payload_two_directions_2( params_two_directions_2, payload_two_directions_2 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_2).payload == payload_two_directions_2 + payload = AngularSeparation(**params_two_directions_2).payload + assert payload == payload_two_directions_2 def test_angular_separation_payload_two_directions_3( @@ -573,7 +576,8 @@ def test_angular_separation_payload_two_directions_3( payload_two_directions_3 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_3).payload == payload_two_directions_3 + payload = AngularSeparation(**params_two_directions_3).payload + assert payload == payload_two_directions_3 def test_angular_separation_payload_two_directions_4( @@ -581,7 +585,8 @@ def test_angular_separation_payload_two_directions_4( payload_two_directions_4 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_4).payload == payload_two_directions_4 + payload = AngularSeparation(**params_two_directions_4).payload + assert payload == payload_two_directions_4 def test_angular_separation_payload_two_directions_5( @@ -589,7 +594,8 @@ def test_angular_separation_payload_two_directions_5( payload_two_directions_5 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_5).payload == payload_two_directions_5 + payload = AngularSeparation(**params_two_directions_5).payload + assert payload == payload_two_directions_5 def test_angular_separation_payload_two_directions_6( @@ -597,4 +603,5 @@ def test_angular_separation_payload_two_directions_6( payload_two_directions_6 ): """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - assert AngularSeparation(**params_two_directions_6).payload == payload_two_directions_6 + payload = AngularSeparation(**params_two_directions_6).payload + assert payload == payload_two_directions_6 From 0b66c92d5da0f2763b41f5c17a3296cfcd04f5ae Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Thu, 6 Mar 2025 11:59:20 +0100 Subject: [PATCH 12/25] Refactor with new test file for Direction class --- tests/test_angular_separation.py | 480 +++---------------------------- tests/test_direction.py | 221 ++++++++++++++ 2 files changed, 255 insertions(+), 446 deletions(-) create mode 100644 tests/test_direction.py diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 54e9e2f..9f2acf3 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -1,10 +1,8 @@ """Test WGC angular separation calculation.""" from pytest import fixture -from pytest import raises from webgeocalc import AngularSeparation -from webgeocalc.errors import CalculationInvalidAttr @fixture @@ -47,7 +45,7 @@ def corr(): @fixture -def params(kernel_paths, time, target_1, target_2, observer, corr): +def params_two_targets(kernel_paths, time, target_1, target_2, observer, corr): """Input parameters from WGC API example.""" return { 'kernel_paths': kernel_paths, @@ -60,7 +58,7 @@ def params(kernel_paths, time, target_1, target_2, observer, corr): @fixture -def payload(kernel_paths, time, target_1, target_2, observer, corr): +def payload_two_targets(kernel_paths, time, target_1, target_2, observer, corr): """Payload from WGC API example.""" return { "kernels": [{ @@ -91,160 +89,6 @@ def kernel_set(): return 5 -@fixture -def direction_vector_inst_boresight(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "INSTRUMENT_BORESIGHT", - "direction_instrument": "CASSINI_ISS_NAC", - } - - -@fixture -def direction_vector_inst_boresight_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "INSTRUMENT_BORESIGHT", - "directionInstrument": "CASSINI_ISS_NAC", - } - - -@fixture -def direction_vector_ref_frame_axis(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "REFERENCE_FRAME_AXIS", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_frame_axis": "Z" - } - - -@fixture -def direction_vector_ref_frame_axis_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "REFERENCE_FRAME_AXIS", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionFrameAxis": "Z" - } - - -@fixture -def direction_vector_in_ref_frame_xyz(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_x": 0.0, - "direction_vector_y": 0.0, - "direction_vector_z": 1.0, - } - - -@fixture -def direction_vector_in_ref_frame_xyz_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorX": 0.0, - "directionVectorY": 0.0, - "directionVectorZ": 1.0, - } - - -@fixture -def direction_vector_in_ref_frame_radec(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_ra": 0.0, - "direction_vector_dec": 0.0, - } - - -@fixture -def direction_vector_in_ref_frame_radec_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorRA": 0.0, - "directionVectorDec": 0.0, - } - - -@fixture -def direction_vector_in_ref_frame_azel(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_az": 0.0, - "direction_vector_el": 0.0, - "azccw_flag": True, - "elplsz_flag": True, - } - - -@fixture -def direction_vector_in_ref_frame_azel_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorAz": 0.0, - "directionVectorEl": 0.0, - "azccwFlag": True, - "elplszFlag": True, - } - - -@fixture -def direction_velocity(): - """Input velocity direction.""" - return { - "direction_type": "VELOCITY", - "target": "CASSINI", - "reference_frame": "IAU_SATURN", - "observer": "SATURN" - } - - -@fixture -def direction_velocity_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VELOCITY", - "target": "CASSINI", - "referenceFrame": "IAU_SATURN", - "observer": "SATURN" - } - - @fixture def direction_position(): """Input position direction.""" @@ -270,239 +114,32 @@ def direction_position_payload(): @fixture -def direction_position_anti_vector_flag_invalid(): - """Input position direction.""" - return { - "direction_type": "POSITION", - "target": "SUN", - "shape": "POINT", - "observer": "CASSINI", - "anti_vector_flag": "Test" - } - - -@fixture -def params_two_directions_anti_vector_flag_invalid( - kernel_set, time, direction_position_anti_vector_flag_invalid, - direction_position, corr -): - """Input parameters for with invalid anti-vector-flag.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_position_anti_vector_flag_invalid, - 'direction_2': direction_position, - 'aberration_correction': corr - } - - -@fixture -def params_two_directions_1( - kernel_set, time, direction_vector_ref_frame_axis, - direction_position, corr -): - """Input parameters from WGC API example.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_vector_ref_frame_axis, - 'direction_2': direction_position, - 'aberration_correction': corr - } - - -@fixture -def payload_two_directions_1( - kernel_set, time, direction_vector_ref_frame_axis_payload, - direction_position_payload, corr -): - """Input parameters from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernel_set, - }], - "specType": "TWO_DIRECTIONS", - "timeSystem": "UTC", - "timeFormat": "CALENDAR", - "times": [ - time, - ], - "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_ref_frame_axis_payload, - "direction2": direction_position_payload, - "aberrationCorrection": corr - } - - -@fixture -def params_two_directions_2( - kernel_set, time, direction_velocity, - direction_position, corr -): - """Input parameters from WGC API example.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_velocity, - 'direction_2': direction_position, - 'aberration_correction': corr - } - - -@fixture -def payload_two_directions_2( - kernel_set, time, direction_velocity_payload, - direction_position_payload, corr -): - """Input parameters from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernel_set, - }], - "specType": "TWO_DIRECTIONS", - "timeSystem": "UTC", - "timeFormat": "CALENDAR", - "times": [ - time, - ], - "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_velocity_payload, - "direction2": direction_position_payload, - "aberrationCorrection": corr - } - - -@fixture -def params_two_directions_3( - kernel_set, time, direction_vector_inst_boresight, - direction_position, corr -): - """Input parameters from WGC API example.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_vector_inst_boresight, - 'direction_2': direction_position, - 'aberration_correction': corr - } - - -@fixture -def payload_two_directions_3( - kernel_set, time, direction_vector_inst_boresight_payload, - direction_position_payload, corr -): - """Input parameters from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernel_set, - }], - "specType": "TWO_DIRECTIONS", - "timeSystem": "UTC", - "timeFormat": "CALENDAR", - "times": [ - time, - ], - "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_inst_boresight_payload, - "direction2": direction_position_payload, - "aberrationCorrection": corr - } - - -@fixture -def params_two_directions_4( - kernel_set, time, - direction_vector_in_ref_frame_xyz, - direction_position, corr -): - """Input parameters from WGC API example.""" - return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_vector_in_ref_frame_xyz, - 'direction_2': direction_position, - 'aberration_correction': corr - } - - -@fixture -def payload_two_directions_4( - kernel_set, time, direction_vector_in_ref_frame_xyz_payload, - direction_position_payload, corr -): - """Input parameters from WGC API example.""" - return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernel_set, - }], - "specType": "TWO_DIRECTIONS", - "timeSystem": "UTC", - "timeFormat": "CALENDAR", - "times": [ - time, - ], - "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_in_ref_frame_xyz_payload, - "direction2": direction_position_payload, - "aberrationCorrection": corr - } - - -@fixture -def params_two_directions_5( - kernel_set, time, - direction_vector_in_ref_frame_radec, - direction_position, corr -): - """Input parameters from WGC API example.""" +def direction_vector(): + """Input vector direction.""" return { - 'spec_type': 'TWO_DIRECTIONS', - 'kernels': kernel_set, - 'times': time, - 'direction_1': direction_vector_in_ref_frame_radec, - 'direction_2': direction_position, - 'aberration_correction': corr + "direction_type": "VECTOR", + "direction_vector_type": "REFERENCE_FRAME_AXIS", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_frame_axis": "Z" } @fixture -def payload_two_directions_5( - kernel_set, time, direction_vector_in_ref_frame_radec_payload, - direction_position_payload, corr -): - """Input parameters from WGC API example.""" +def direction_vector_payload(): + """Vector direction payload.""" return { - "kernels": [{ - "type": "KERNEL_SET", - "id": kernel_set, - }], - "specType": "TWO_DIRECTIONS", - "timeSystem": "UTC", - "timeFormat": "CALENDAR", - "times": [ - time, - ], - "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_in_ref_frame_radec_payload, - "direction2": direction_position_payload, - "aberrationCorrection": corr + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "REFERENCE_FRAME_AXIS", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionFrameAxis": "Z" } @fixture -def params_two_directions_6( - kernel_set, time, - direction_vector_in_ref_frame_azel, +def params_two_directions( + kernel_set, time, direction_vector, direction_position, corr ): """Input parameters from WGC API example.""" @@ -510,15 +147,15 @@ def params_two_directions_6( 'spec_type': 'TWO_DIRECTIONS', 'kernels': kernel_set, 'times': time, - 'direction_1': direction_vector_in_ref_frame_azel, + 'direction_1': direction_vector, 'direction_2': direction_position, 'aberration_correction': corr } @fixture -def payload_two_directions_6( - kernel_set, time, direction_vector_in_ref_frame_azel_payload, +def payload_two_directions( + kernel_set, time, direction_vector_payload, direction_position_payload, corr ): """Input parameters from WGC API example.""" @@ -534,74 +171,25 @@ def payload_two_directions_6( time, ], "calculationType": "ANGULAR_SEPARATION", - "direction1": direction_vector_in_ref_frame_azel_payload, + "direction1": direction_vector_payload, "direction2": direction_position_payload, "aberrationCorrection": corr } -def test_angular_separation_payload(params, payload): - """Test angular separation payload.""" - assert AngularSeparation(**params).payload == payload - - -def test_angular_separation_payload_two_directions_1( - params_two_directions_1, - payload_two_directions_1 -): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_1).payload - assert payload == payload_two_directions_1 - - -def test_angular_separation_payload_two_directions_1_anti_vector_flag_error( - params_two_directions_anti_vector_flag_invalid -): - """Test anti-vector flag error.""" - with raises(CalculationInvalidAttr): - AngularSeparation(**params_two_directions_anti_vector_flag_invalid) - - -def test_angular_separation_payload_two_directions_2( - params_two_directions_2, - payload_two_directions_2 -): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_2).payload - assert payload == payload_two_directions_2 - - -def test_angular_separation_payload_two_directions_3( - params_two_directions_3, - payload_two_directions_3 -): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_3).payload - assert payload == payload_two_directions_3 - - -def test_angular_separation_payload_two_directions_4( - params_two_directions_4, - payload_two_directions_4 -): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_4).payload - assert payload == payload_two_directions_4 - - -def test_angular_separation_payload_two_directions_5( - params_two_directions_5, - payload_two_directions_5 +def test_angular_separation_payload_two_targets( + params_two_targets, + payload_two_targets ): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_5).payload - assert payload == payload_two_directions_5 + """Test angular separation payload (``TWO_TARGETS`` mode).""" + payload = AngularSeparation(**params_two_targets).payload + assert payload == payload_two_targets -def test_angular_separation_payload_two_directions_6( - params_two_directions_6, - payload_two_directions_6 +def test_angular_separation_payload_two_directions( + params_two_directions, + payload_two_directions ): - """Test angular separation payload (`TWO_DIRECTIONS` mode).""" - payload = AngularSeparation(**params_two_directions_6).payload - assert payload == payload_two_directions_6 + """Test angular separation payload (``TWO_DIRECTIONS`` mode).""" + payload = AngularSeparation(**params_two_directions).payload + assert payload == payload_two_directions diff --git a/tests/test_direction.py b/tests/test_direction.py new file mode 100644 index 0000000..73d788d --- /dev/null +++ b/tests/test_direction.py @@ -0,0 +1,221 @@ +"""Test WGC direction setup.""" + +import pytest +from pytest import fixture, raises + +from webgeocalc.calculation import _Direction +from webgeocalc.errors import CalculationInvalidAttr + + +@fixture +def direction_vector_inst_boresight(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "INSTRUMENT_BORESIGHT", + "direction_instrument": "CASSINI_ISS_NAC", + } + + +@fixture +def direction_vector_inst_boresight_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "INSTRUMENT_BORESIGHT", + "directionInstrument": "CASSINI_ISS_NAC", + } + + +@fixture +def direction_vector_ref_frame_axis(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "REFERENCE_FRAME_AXIS", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_frame_axis": "Z" + } + + +@fixture +def direction_vector_ref_frame_axis_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "REFERENCE_FRAME_AXIS", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionFrameAxis": "Z" + } + + +@fixture +def direction_vector_in_ref_frame_xyz(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_x": 0.0, + "direction_vector_y": 0.0, + "direction_vector_z": 1.0, + } + + +@fixture +def direction_vector_in_ref_frame_xyz_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorX": 0.0, + "directionVectorY": 0.0, + "directionVectorZ": 1.0, + } + + +@fixture +def direction_vector_in_ref_frame_radec(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_ra": 0.0, + "direction_vector_dec": 0.0, + } + + +@fixture +def direction_vector_in_ref_frame_radec_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorRA": 0.0, + "directionVectorDec": 0.0, + } + + +@fixture +def direction_vector_in_ref_frame_azel(): + """Input vector direction.""" + return { + "direction_type": "VECTOR", + "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", + "direction_frame": "CASSINI_RPWS_EDIPOLE", + "direction_vector_az": 0.0, + "direction_vector_el": 0.0, + "azccw_flag": True, + "elplsz_flag": True, + } + + +@fixture +def direction_vector_in_ref_frame_azel_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionVectorAz": 0.0, + "directionVectorEl": 0.0, + "azccwFlag": True, + "elplszFlag": True, + } + + +@fixture +def direction_velocity(): + """Input velocity direction.""" + return { + "direction_type": "VELOCITY", + "target": "CASSINI", + "reference_frame": "IAU_SATURN", + "observer": "SATURN" + } + + +@fixture +def direction_velocity_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VELOCITY", + "target": "CASSINI", + "referenceFrame": "IAU_SATURN", + "observer": "SATURN" + } + + +@fixture +def direction_position(): + """Input position direction.""" + 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_position_anti_vector_flag_invalid(): + """Input position direction.""" + return { + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI", + "anti_vector_flag": "Test" + } + + +@pytest.mark.parametrize("fixture_name", [ + "direction_vector_inst_boresight", + "direction_vector_ref_frame_axis", + "direction_vector_in_ref_frame_xyz", + "direction_vector_in_ref_frame_radec", + "direction_vector_in_ref_frame_azel", + "direction_velocity", + "direction_position", +]) +def test_direction_vector_in_ref_frame(request, fixture_name): + """Test direction payload.""" + params = request.getfixturevalue(fixture_name) + payload = request.getfixturevalue(fixture_name + '_payload') + assert _Direction(**params).payload == payload + + +def test_direction_position_anti_vector_flag_invalid_error( + direction_position_anti_vector_flag_invalid +): + """Test anti-vector flag error.""" + with raises(CalculationInvalidAttr): + _Direction(**direction_position_anti_vector_flag_invalid) From 392d46bf08870f1da7a421c93abd582b63b4cc16 Mon Sep 17 00:00:00 2001 From: Cecconi Baptiste Date: Thu, 6 Mar 2025 17:12:21 +0100 Subject: [PATCH 13/25] Add no cover flag --- tests/test_decorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 6854597..6f484eb 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -29,7 +29,7 @@ def baz(self, value): @parameter(only='WRONG') def qux(self, value): """Dummy parameter with an invalid ``only``.""" - self.qux_ = value + self.qux_ = value # pragma: no cover a = A() From 32dd97a288dc6f3dfe22e39beb2f04ccfc4536fa Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Thu, 6 Mar 2025 17:53:00 +0100 Subject: [PATCH 14/25] Remove no cover --- tests/test_decorator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 6f484eb..6545ea7 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -29,7 +29,6 @@ def baz(self, value): @parameter(only='WRONG') def qux(self, value): """Dummy parameter with an invalid ``only``.""" - self.qux_ = value # pragma: no cover a = A() From bb29c6b7ec4de7be54be6a5e828e17d8a803f92f Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 7 Mar 2025 10:21:31 +0100 Subject: [PATCH 15/25] Reorder vars --- webgeocalc/vars.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 3a820f6..8dec34d 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -105,6 +105,15 @@ 'XCN', 'S', 'XS', + ], + 'SPEC_TYPE': [ + 'TWO_TARGETS', + 'TWO_DIRECTIONS', + ], + 'DIRECTION_TYPE': [ + 'POSITION', + 'VELOCITY', + 'VECTOR', ], 'STATE_REPRESENTATION': [ 'RECTANGULAR', @@ -121,6 +130,10 @@ 'ELLIPSOID', 'DSK ', ], + 'TARGET_SHAPE': [ + 'POINT', + 'SPHERE', + ], 'TIME_LOCATION': [ 'FRAME1', 'FRAME2', @@ -216,17 +229,4 @@ 'LOCMAX', 'LOCMIN', ], - "SPEC_TYPE": [ - "TWO_TARGETS", - "TWO_DIRECTIONS", - ], - "TARGET_SHAPE": [ - "POINT", - "SPHERE", - ], - "DIRECTION_TYPE": [ - "POSITION", - "VELOCITY", - "VECTOR", - ], } From 02d0429a0b7409049e88eb5b7269785adc9ef9f0 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 7 Mar 2025 11:25:24 +0100 Subject: [PATCH 16/25] Bump requests version (closes #9) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8f20977..9382233 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ version='1.4.0', description='Python package for NAIF WebGeoCalc API', author='Benoit Seignovert', - author_email='benoit.a.seignovert@univ-nantes.fr', + author_email='benoit.seignovert@univ-nantes.fr', url='https://github.com/seignovert/python-webgeocalc', project_urls={ 'Bug Tracker': 'https://github.com/seignovert/python-webgeocalc/issues', @@ -24,7 +24,7 @@ license='MIT', python_requires='>=3.8', install_requires=[ - 'requests==2.25.1', + 'requests>=2.31', ], packages=find_packages(), include_package_data=False, From 770f3c9efbce90a67d591479e907244e86377c43 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 7 Mar 2025 11:26:45 +0100 Subject: [PATCH 17/25] Bump python min version to 3.11 --- .github/workflows/release.yml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa49926..6bcf554 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release env: PACKAGE: webgeocalc - PYTHON: 3.8 + PYTHON: 3.11 on: push: diff --git a/setup.py b/setup.py index 9382233..e3f1385 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ 'Documentation': 'https://webgeocalc.readthedocs.io/', }, license='MIT', - python_requires='>=3.8', + python_requires='>=3.11', install_requires=[ 'requests>=2.31', ], From cd1ef7562da49594fbd1722e27088e53f0464079 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 7 Mar 2025 13:52:53 +0100 Subject: [PATCH 18/25] Refactor Calculation to extract Payload behavior in abstract class --- tests/test_payload.py | 62 ++++++++++++++++++++++++++++++++++ webgeocalc/calculation.py | 51 +++++----------------------- webgeocalc/errors.py | 2 +- webgeocalc/payload.py | 70 +++++++++++++++++++++++++++++++++++++++ webgeocalc/vars.py | 2 +- 5 files changed, 142 insertions(+), 45 deletions(-) create mode 100644 tests/test_payload.py create mode 100644 webgeocalc/payload.py diff --git a/tests/test_payload.py b/tests/test_payload.py new file mode 100644 index 0000000..d997888 --- /dev/null +++ b/tests/test_payload.py @@ -0,0 +1,62 @@ +"""Test WebGeoCalc payload abstract class.""" + +from pytest import raises + +from webgeocalc.decorator import parameter +from webgeocalc.errors import CalculationRequiredAttr +from webgeocalc.payload import Payload + + +def test_payload_abstract(): + """Test payload abstract class.""" + p = Payload(foo='bar') + + # Input parameters + assert p.params == {'foo': 'bar'} + + # No parameter explicitly defined (with @parameter) + assert not p.payload + + # Keywords are set as regular properties + getattr(p, 'foo', 'bar') + + assert repr(p) == '' + + +def test_payload_derived(): + """Test payload derived class.""" + class DerivedPayload(Payload): + """Derived payload class.""" + + REQUIRED = ('foo',) + + @parameter # required + def foo(self, val): + """Foo parameter.""" + self.__foo = val + + @parameter(only='AXIS') # optional + def baz(self, val): + """Baz parameter.""" + self.__baz = val + + # With all parameters (and more) + d = DerivedPayload(foo='bar', baz='X', qux='quux') + + assert d.params == {'foo': 'bar', 'baz': 'X', 'qux': 'quux'} + assert d.payload == {'foo': 'bar', 'baz': 'X'} # parameters only + + assert repr(d) == '\n - foo: bar\n - baz: X' + + # __iter__ + for key, value in d: + assert key == 'foo' + assert value == 'bar' + break + + # Only with required parameter(s) + assert DerivedPayload(foo='bar').payload == {'foo': 'bar'} + + # Without required parameter(s) + with raises(CalculationRequiredAttr): + _ = DerivedPayload() diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index bd28d00..023e2fb 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -9,6 +9,7 @@ CalculationInvalidAttr, CalculationInvalidValue, CalculationNotCompleted, CalculationRequiredAttr, CalculationTimeOut, CalculationUndefinedAttr) +from .payload import Payload from .types import KernelSetDetails from .vars import CALCULATION_FAILED_PHASES, VALID_PARAMETERS @@ -20,7 +21,7 @@ } -class Calculation: +class Calculation(Payload): """Webgeocalc calculation object. Parameters @@ -151,16 +152,13 @@ class Calculation: """ - REQUIRED = () - def __init__(self, api='', time_system='UTC', time_format='CALENDAR', verbose=True, **kwargs): # Add default parameters to kwargs kwargs['time_system'] = time_system kwargs['time_format'] = time_format - # Init parameters - self.params = kwargs + # Init other parameters self.__kernels = [] self.id = None self.phase = 'NOT SUBMITTED' @@ -182,12 +180,11 @@ def __init__(self, api='', time_system='UTC', if 'times' not in kwargs and 'intervals' not in kwargs: raise CalculationRequiredAttr("times' or 'intervals") - self._required('calculation_type', 'time_system', 'time_format', - *self.REQUIRED) + # Prepend calculation required parameters + self.REQUIRED = ('calculation_type', 'time_system', 'time_format') + self.REQUIRED - # Set parameters - for key, value in kwargs.items(): - setattr(self, key, value) + # Set all parameters + super().__init__(**kwargs) def __repr__(self): return '\n'.join([ @@ -196,38 +193,6 @@ def __repr__(self): f' - {k}: {v}' for k, v in self.payload.items() ]) - def _required(self, *attrs): - """Check if the required arguments are in the params.""" - for attr in attrs: - if attr not in self.params: - raise CalculationRequiredAttr(attr) - - @property - def payload(self): - """Calculation payload parameters *dict* for JSON input in WebGeoCalc format. - - Return - ------ - dict - Payload keys and values. - - Example - ------- - >>> Calculation( - ... kernels = 'Cassini Huygens', - ... times = '2012-10-19T08:24:00.000', - ... calculation_type = 'STATE_VECTOR', - ... target = 'CASSINI', - ... observer = 'SATURN', - ... reference_frame = 'IAU_SATURN', - ... aberration_correction = 'NONE', - ... state_representation = 'PLANETOGRAPHIC', - ... ).payload # noqa: E501 - {'kernels': [{'type': 'KERNEL_SET', 'id': 5}], 'times': ['2012-10-19T08:24:00.000'], ...} - - """ - return {k.split('__')[-1]: v for k, v in vars(self).items() if k.startswith('_')} - def submit(self): """Submit calculation parameters and get calculation ``id`` and ``phase``. @@ -469,7 +434,7 @@ def kernel_paths(self, paths): @staticmethod def _kernel_path_obj(server_path): - # Payloaf individual kernel path object + # Payload individual kernel path object return {"type": "KERNEL", "path": server_path} @parameter diff --git a/webgeocalc/errors.py b/webgeocalc/errors.py index b7aeb56..1dea7c8 100644 --- a/webgeocalc/errors.py +++ b/webgeocalc/errors.py @@ -132,5 +132,5 @@ class CalculationTimeOut(IOError): def __init__(self, timeout, sleep): msg = f'Calculation time-out after {timeout} seconds' + \ - f' ({int(timeout/sleep)} attempts)' + f' ({int(timeout / sleep)} attempts)' super().__init__(msg) diff --git a/webgeocalc/payload.py b/webgeocalc/payload.py new file mode 100644 index 0000000..6d9cd23 --- /dev/null +++ b/webgeocalc/payload.py @@ -0,0 +1,70 @@ +"""WebGeoCalc Payload.""" + +from .errors import CalculationRequiredAttr + + +class Payload: + """Abstract WebGeoCalc payload class. + + Check if any required parameters is missing. + + Raises + ------ + CalculationRequiredAttr + If any parameter in :py:attr:`REQUIRED` is not provided. + + """ + + REQUIRED = () + + def __init__(self, **kwargs): + # Init parameters + self.params = kwargs + + # Check required parameters + self._required(*self.REQUIRED) + + # Set parameters + for key, value in kwargs.items(): + setattr(self, key, value) + + def __repr__(self): + return '\n'.join([ + f"<{self.__class__.__name__}>" + ] + [ + f' - {k}: {v}' for k, v in self + ]) + + def __iter__(self): + return ( + (k.split('__')[-1], v) + for k, v in vars(self).items() + if k.startswith('_') + ) + + def _required(self, *attrs): + """Check if the required arguments are in the params.""" + for attr in attrs: + if attr not in self.params: + raise CalculationRequiredAttr(attr) + + @property + def payload(self) -> dict: + """Payload parameters *dict* for JSON input in WebGeoCalc format. + + Collect all the properties prefixed with `__`. + + Return + ------ + dict + Payload keys and values. + + Example + ------- + >>> Payload( + ... foo = 'bar', + ... ).payload # noqa: E501 + {'foo': 'bar'} + + """ + return dict(self) diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 8dec34d..317fda6 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -106,7 +106,7 @@ 'S', 'XS', ], - 'SPEC_TYPE': [ + 'SPEC_TYPE': [ 'TWO_TARGETS', 'TWO_DIRECTIONS', ], From 714bfae1594a99cafcad58a811fa64b94d55eb70 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 7 Mar 2025 16:44:00 +0100 Subject: [PATCH 19/25] Refactor Direction to isolate them from the Calculation [skip ci] --- tests/test_cli.py | 10 + tests/test_direction.py | 420 ++++++++++++++++----- webgeocalc/calculation.py | 526 +++----------------------- webgeocalc/calculation_types.py | 38 +- webgeocalc/cli.py | 18 +- webgeocalc/direction.py | 633 ++++++++++++++++++++++++++++++++ webgeocalc/vars.py | 34 +- 7 files changed, 1078 insertions(+), 601 deletions(-) create mode 100644 webgeocalc/direction.py diff --git a/tests/test_cli.py b/tests/test_cli.py index d433fee..42ce4ea 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -164,6 +164,16 @@ def test_cli_state_vector_empty(capsys): assert 'usage:' in captured.out +def test_cli_angular_separation_two_targets_dry_run(capsys): + """Test dry-run angular separation calculation for 2 targets with the CLI.""" + assert False # FIXME + + +def test_cli_angular_separation_two_directions_dry_run(capsys): + """Test dry-run angular separation calculation for 2 direction with the CLI.""" + assert False # FIXME + + def test_cli_angular_separation_wrong_attr(capsys): """Test attribute in angular separation calculation parameter with the CLI.""" argv = '--kernels 1 --times 2012-10-19T08:24:00 --wrong 123'.split() diff --git a/tests/test_direction.py b/tests/test_direction.py index 73d788d..eb57b30 100644 --- a/tests/test_direction.py +++ b/tests/test_direction.py @@ -3,56 +3,274 @@ import pytest from pytest import fixture, raises -from webgeocalc.calculation import _Direction -from webgeocalc.errors import CalculationInvalidAttr +from webgeocalc.direction import Direction +from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr + + +def test_direction_position_with_shape(): + """Test direction position type with shape (for `AngularSeparation`).""" + assert Direction( + direction_type='POSITION', + target='MARS', + shape='POINT', + observer='EARTH', + ).payload == { + 'directionType': 'POSITION', + 'target': 'MARS', + 'shape': 'POINT', + 'observer': 'EARTH', + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + } +def test_direction_position_without_shape(): + """Test direction position type without shape (for `PointingDirection`).""" + assert Direction( + direction_type='POSITION', + target='MARS', + observer='EARTH', + aberration_correction='LT+S', + anti_vector_flag='TRUE', # Uppercase + ).payload == { + 'directionType': 'POSITION', + 'target': 'MARS', + 'observer': 'EARTH', + 'aberrationCorrection': 'LT+S', + 'antiVectorFlag': True, + } -@fixture -def direction_vector_inst_boresight(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "INSTRUMENT_BORESIGHT", - "direction_instrument": "CASSINI_ISS_NAC", + +def test_direction_position_errors(): + """Test direction position type errors """ + # Missing target + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='POSITION', + observer='EARTH', + ) + + # Missing observer + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='POSITION', + target='MARS', + shape='POINT', + ) + + # Invalid shape + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='POSITION', + target='MARS', + shape='WRONG', + observer='EARTH', + ) + + # Invalid aberration correction + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='POSITION', + target='MARS', + shape='SPHERE', + observer='EARTH', + aberration_correction='S', + ) + + # Invalid anti vector flag when shape is sphere + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='POSITION', + target='MARS', + shape='SPHERE', + observer='EARTH', + anti_vector_flag=True, + ) + + +def test_direction_velocity(): + """Test direction velocity type.""" + assert Direction( + direction_type='VELOCITY', + target='MARS', + reference_frame='itrf93', # lowercase + observer='EARTH', + aberration_correction='XCN+S', + anti_vector_flag='true', # Lowercase + ).payload == { + 'directionType': 'VELOCITY', + 'target': 'MARS', + 'referenceFrame': 'ITRF93', + 'observer': 'EARTH', + 'aberrationCorrection': 'XCN+S', + 'antiVectorFlag': True, } -@fixture -def direction_vector_inst_boresight_payload(): - """Vector direction payload.""" - return { +def test_direction_position_errors(): + """Test direction position type errors """ + # Missing target + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VELOCITY', + reference_frame='ITRF93', + observer='EARTH', + ) + + # Missing reference frame + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VELOCITY', + target='MARS', + observer='EARTH', + ) + + # Missing observer + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VELOCITY', + target='MARS', + reference_frame='ITRF93', + ) + + +def test_direction_vector_inst_boresight(): + """Test direction vector for instrument boresight.""" + # Without observer (aberration correction NONE) + assert Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + ).payload == { + 'directionType': 'VECTOR', + 'directionVectorType': 'INSTRUMENT_BORESIGHT', + 'directionInstrument': 'CASSINI_ISS_NAC', 'aberrationCorrection': 'NONE', 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "INSTRUMENT_BORESIGHT", - "directionInstrument": "CASSINI_ISS_NAC", } + # With explicit observer (aberration correction ≠ NONE) + assert Direction( + direction_type='VECTOR', + observer='CASSINI', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + aberration_correction='CN' + ).payload == { + 'directionType': 'VECTOR', + 'observer': 'CASSINI', + 'directionVectorType': 'INSTRUMENT_BORESIGHT', + 'directionInstrument': 'CASSINI_ISS_NAC', + 'aberrationCorrection': 'CN', + 'antiVectorFlag': False, + } -@fixture -def direction_vector_ref_frame_axis(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "REFERENCE_FRAME_AXIS", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_frame_axis": "Z" + +def test_direction_vector_ref_frame_axis(): + """Test direction vector for reference fame axis.""" + assert Direction( + direction_type='VECTOR', + observer='EARTH', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='IAU_EARTH', + direction_frame_axis='X', + aberration_correction='S', + anti_vector_flag=True, + ).payload == { + 'directionType': 'VECTOR', + 'observer': 'EARTH', + 'directionVectorType': 'REFERENCE_FRAME_AXIS', + 'directionFrame': 'IAU_EARTH', + 'directionFrameAxis': 'X', + 'aberrationCorrection': 'S', + 'antiVectorFlag': True, } -@fixture -def direction_vector_ref_frame_axis_payload(): - """Vector direction payload.""" - return { +def test_direction_vector_in_instr_fov(): + """Test direction vector for in instrument FOV.""" + assert Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + direction_instrument='CASSINI_RPWS_EDIPOLE', + direction_frame_axis='Z', + ).payload == { + 'directionType': 'VECTOR', + 'directionVectorType': 'VECTOR_IN_INSTRUMENT_FOV', + 'directionInstrument': 'CASSINI_RPWS_EDIPOLE', + 'directionFrameAxis': 'Z', 'aberrationCorrection': 'NONE', 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "REFERENCE_FRAME_AXIS", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionFrameAxis": "Z" } +def test_direction_vector_errors(): + """Test direction vector errors.""" + # Missing direction_vector_type + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + ) + + # Missing observer with aberration correction NONE + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + aberration_correction='CN', + ) + + # + +## ---- Remove below FIXME ----- + + +# @fixture +# def direction_vector_inst_boresight(): +# """Input vector direction.""" +# return { +# "direction_type": "VECTOR", +# "direction_vector_type": "INSTRUMENT_BORESIGHT", +# "direction_instrument": "CASSINI_ISS_NAC", +# } + + +# @fixture +# def direction_vector_inst_boresight_payload(): +# """Vector direction payload.""" +# return { +# 'aberrationCorrection': 'NONE', +# 'antiVectorFlag': False, +# "directionType": "VECTOR", +# "directionVectorType": "INSTRUMENT_BORESIGHT", +# "directionInstrument": "CASSINI_ISS_NAC", +# } + + +# @fixture +# def direction_vector_ref_frame_axis(): +# """Input vector direction.""" +# return { +# "direction_type": "VECTOR", +# "direction_vector_type": "REFERENCE_FRAME_AXIS", +# "direction_frame": "CASSINI_RPWS_EDIPOLE", +# "direction_frame_axis": "Z" +# } + + +# @fixture +# def direction_vector_ref_frame_axis_payload(): +# """Vector direction payload.""" +# return { +# 'aberrationCorrection': 'NONE', +# 'antiVectorFlag': False, +# "directionType": "VECTOR", +# "directionVectorType": "REFERENCE_FRAME_AXIS", +# "directionFrame": "CASSINI_RPWS_EDIPOLE", +# "directionFrameAxis": "Z" +# } + + @fixture def direction_vector_in_ref_frame_xyz(): """Input vector direction.""" @@ -137,85 +355,85 @@ def direction_vector_in_ref_frame_azel_payload(): } -@fixture -def direction_velocity(): - """Input velocity direction.""" - return { - "direction_type": "VELOCITY", - "target": "CASSINI", - "reference_frame": "IAU_SATURN", - "observer": "SATURN" - } - - -@fixture -def direction_velocity_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VELOCITY", - "target": "CASSINI", - "referenceFrame": "IAU_SATURN", - "observer": "SATURN" - } - - -@fixture -def direction_position(): - """Input position direction.""" - 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_position_anti_vector_flag_invalid(): - """Input position direction.""" - return { - "direction_type": "POSITION", - "target": "SUN", - "shape": "POINT", - "observer": "CASSINI", - "anti_vector_flag": "Test" - } +# @fixture +# def direction_velocity(): +# """Input velocity direction.""" +# return { +# "directionType": "VELOCITY", +# "target": "CASSINI", +# "referenceFrame": "IAU_SATURN", +# "observer": "SATURN" +# } + + +# @fixture +# def direction_velocity_payload(): +# """Vector direction payload.""" +# return { +# 'aberrationCorrection': 'NONE', +# 'antiVectorFlag': False, +# "directionType": "VELOCITY", +# "target": "CASSINI", +# "referenceFrame": "IAU_SATURN", +# "observer": "SATURN" +# } + + +# @fixture +# def direction_position(): +# """Input position direction.""" +# 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_position_anti_vector_flag_invalid(): +# """Input position direction.""" +# return { +# "directionType": "POSITION", +# "target": "SUN", +# "shape": "POINT", +# "observer": "CASSINI", +# "antiVectorFlag": "Test" +# } @pytest.mark.parametrize("fixture_name", [ - "direction_vector_inst_boresight", - "direction_vector_ref_frame_axis", + # "direction_vector_inst_boresight", + # "direction_vector_ref_frame_axis", "direction_vector_in_ref_frame_xyz", "direction_vector_in_ref_frame_radec", "direction_vector_in_ref_frame_azel", - "direction_velocity", - "direction_position", + # "direction_velocity", + # "direction_position", ]) def test_direction_vector_in_ref_frame(request, fixture_name): """Test direction payload.""" params = request.getfixturevalue(fixture_name) payload = request.getfixturevalue(fixture_name + '_payload') - assert _Direction(**params).payload == payload + assert Direction(**params).payload == payload -def test_direction_position_anti_vector_flag_invalid_error( - direction_position_anti_vector_flag_invalid -): - """Test anti-vector flag error.""" - with raises(CalculationInvalidAttr): - _Direction(**direction_position_anti_vector_flag_invalid) +# def test_direction_position_anti_vector_flag_invalid_error( +# direction_position_anti_vector_flag_invalid +# ): +# """Test anti-vector flag error.""" +# with raises(CalculationInvalidAttr): +# Direction(**direction_position_anti_vector_flag_invalid) diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index 023e2fb..cdeac36 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -4,6 +4,7 @@ from .api import API, Api, ESA_API, JPL_API from .decorator import parameter +from .direction import Direction from .errors import (CalculationAlreadySubmitted, CalculationConflictAttr, CalculationFailed, CalculationIncompatibleAttr, CalculationInvalidAttr, CalculationInvalidValue, @@ -95,6 +96,12 @@ class Calculation(Payload): See: :py:attr:`center_body` aberration_correction: str See: :py:attr:`aberration_correction` + spec_type: str + See: :py:attr:`spec_type` + direction_1: str + See: :py:attr:`direction_1` + direction_2: str + See: :py:attr:`direction_2` state_representation: str See: :py:attr:`state_representation` time_location: str @@ -1021,6 +1028,60 @@ def aberration_correction(self, val): """ self.__aberrationCorrection = val + @parameter(only='SPEC_TYPE') + def spec_type(self, val): + """Angular separation computation type. + + Method used to specify the directions between which + the angular separation is computed. + + Parameters + ---------- + spec_type: str + One of the following: + + - TWO_TARGETS + - TWO_DIRECTIONS + + """ + self.__specType = val + + @parameter + def direction_1(self, val): + """The first direction object. + + Definition of first direction for two-directions angular + separation calculation. + + Parameters + ---------- + direction_1: dict or Direction + Direction vector. See: :py:class:`Direction`. + + """ + if not isinstance(val, Direction): + val = Direction(**val) + + self.__direction1 = val.payload + + @parameter + def direction_2(self, val): + """The second direction object. + + Definition of second direction for two-directions angular + separation calculation. + + Parameters + ---------- + direction_2: dict or Direction + Direction vector. See: :py:class:`Direction`. + + """ + if not isinstance(val, Direction): + val = Direction(**val) + + self.__direction2 = val.payload + @parameter(only='STATE_REPRESENTATION') def state_representation(self, val): """State representation. @@ -2006,468 +2067,3 @@ def gf_condition(self, **kwargs): ) from None self.__condition = kwargs - - @parameter - def spec_type(self, val): - """Type of angular separation calculation. - - Parameters - ---------- - spec_type: str - One of the following: - - - TWO_TARGETS - - TWO_DIRECTIONS - - """ - self.__specType = val - - @parameter - def direction_1(self, val): - """The first direction object. - - Definition of first direction for two-directions angular - separation calculation. - - Parameters - ---------- - direction_1: dict - Direction configuration. See: :py:func:`direction`. - - """ - self.__direction1 = _Direction(**val).payload - - @parameter - def direction_2(self, val): - """The second direction object. - - Definition of second direction for two-directions angular - separation calculation. - - Parameters - ---------- - direction_2: dict - Direction configuration. See: :py:class:`_Direction`. - - """ - self.__direction2 = _Direction(**val).payload - - -class _Direction: - """Webgeocalc direction object.""" - - def __init__(self, aberration_correction="NONE", - anti_vector_flag=False, **kwargs): - - if kwargs["direction_type"] == "VECTOR": - kwargs["aberration_correction_vector"] = aberration_correction - else: - kwargs['aberration_correction'] = aberration_correction - - kwargs['anti_vector_flag'] = anti_vector_flag - - for key, value in kwargs.items(): - setattr(self, key, value) - - @property - def payload(self): - """Direction payload parameters *dict* for JSON input in WebGeoCalc format. - - Return - ------ - dict - Payload keys and values. - - """ - return {k.split('__')[-1]: v for k, v in vars(self).items() if k.startswith('_')} - - @parameter(only='ABERRATION_CORRECTION') - def aberration_correction(self, val): - """SPICE aberration correction. - - Parameters - ---------- - aberration_correction: str - The SPICE aberration correction string. One of: - - - NONE - - LT - - LT+S - - CN - - CN+S - - XLT - - XLT+S - - XCN - - XCN+S - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__aberrationCorrection = val - - @parameter(only='DIRECTION_TYPE') - def direction_type(self, val): - """Type of direction. - - Method used to specify a direction. Directions could be specified as the - position of an object as seen from the observer, as the velocity vector of - an object as seen from the observer in a given reference frame, or by - providing a vector in a given reference frame. - - Parameters - ---------- - direction_type: str - The type of direction string. One of: - - - POSITION - - VELOCITY - - VECTOR - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__directionType = val - - @parameter - def anti_vector_flag(self, val): - """Anti Vector Flag. - - Parameters - ---------- - anti_vector_flag: bool - `True` if the anti-vector shall be used for the direction, and `False` - otherwise. Required when the target shape is `POINT`. If provided when - the target shape is `SPHERE`, it must be set to false, i.e., using - anti-vector direction is not supported for target bodies modeled as - spheres. - - """ - if not isinstance(val, bool): - raise CalculationInvalidAttr( - name="ANTI_VECTOR_FLAG", - attr=val, - valids=["True", "False"] - ) - self.__antiVectorFlag = val - - @parameter - def target(self, val): - """Target body. - - Parameters - ---------- - target: str or int - The target body ``name`` or ``id`` from :py:func:`API.bodies`. - - """ - self.__target = val if isinstance(val, int) else val.upper() - - @parameter(only='TARGET_SHAPE') - def shape(self, val): - """The shape to use for the first body. - - Parameters - ---------- - shape: str - One of: - - POINT - - SPHERE - - Raises - ------- - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__shape = val - - @parameter - def observer(self, val): - """Observing body. - - Parameters - ---------- - observer: str or int - The observing body ``name`` or ``id`` from :py:func:`API.bodies`. - - """ - self.__observer = val if isinstance(val, int) else val.upper() - - @parameter - def reference_frame(self, val): - """The reference frame name. - - Parameters - ---------- - reference_frame: str - The reference frame name. - - """ - self.__referenceFrame = val.upper() - - @parameter(only='ABERRATION_CORRECTION_VECTOR') - def aberration_correction_vector(self, val): - """SPICE aberration correction. - - Parameters - ---------- - aberration_correction: str - The SPICE aberration correction string. One of: - - - NONE - - LT - - CN - - XLT - - XCN - - S - - XS - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__aberrationCorrection = val - - @parameter(only='DIRECTION_VECTOR_TYPE') - def direction_vector_type(self, val): - """Direction vector type. - - Parameters - ---------- - direction_vector_type: str - The direction vector type string. One of: - - - INSTRUMENT_BORESIGHT - - INSTRUMENT_FOV_BOUNDARY_VECTORS - - REFERENCE_FRAME_AXIS - - VECTOR_IN_INSTRUMENT_FOV - - VECTOR_IN_REFERENCE_FRAME - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__directionVectorType = val - - @parameter - def direction_instrument(self, val): - """The instrument name or ID. - - Required only if directionVectorType is INSTRUMENT_BORESIGHT, - VECTOR_IN_INSTRUMENT_FOV or INSTRUMENT_FOV_BOUNDARY_VECTORS. - - Parameters - ---------- - direction_instrument: str or int - The instrument ``name`` or ``ID``. - - """ - self.__directionInstrument = val if isinstance(val, int) else val.upper() - - @parameter - def direction_frame(self, val): - """The vector's reference frame name. - - Required only if directionVectorType is REFERENCE_FRAME_AXIS - or VECTOR_IN_REFERENCE_FRAME. - - Parameters: - ----------- - direction_frame: str - The vector's reference frame name. - - """ - self.__directionFrame = val - - @parameter(only='AXIS') - def direction_frame_axis(self, val): - """The direction vector frame axis. - - Required only if directionVectorType is REFERENCE_FRAME_AXIS. - - Parameters - ---------- - direction_frame_axis: str - The direction frame axis string. One of: - - - X - - Y - - Z - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - - """ - self.__directionFrameAxis = val - - @parameter - def direction_vector_x(self, val): - """The X direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_x: float - The X direction vector coordinate value. - - """ - self.__directionVectorX = val - - @parameter - def direction_vector_y(self, val): - """The Y direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_y: float - The Y direction vector coordinate value. - - """ - self.__directionVectorY = val - - @parameter - def direction_vector_z(self, val): - """The Z direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_z: float - The Z direction vector coordinate value. - - """ - self.__directionVectorZ = val - - @parameter - def direction_vector_ra(self, val): - """The right ascension direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_ra: float - The right ascension direction vector coordinate value. - - """ - self.__directionVectorRA = val - - @parameter - def direction_vector_dec(self, val): - """The declination direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_dec: float - The declination direction vector coordinate value. - - """ - self.__directionVectorDec = val - - @parameter - def direction_vector_az(self, val): - """The azimuth direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_az: float - The azimuth direction vector coordinate value. - - """ - self.__directionVectorAz = val - - @parameter - def direction_vector_el(self, val): - """The elevation direction vector coordinate. - - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. - - Parameter - --------- - direction_vector_el: float - The elevation vector coordinate value. - - """ - self.__directionVectorEl = val - - @parameter - def azccw_flag(self, val): - """Flag indicating how azimuth is measured. - - If azccwFlag is ``true``, azimuth increases in the counterclockwise - direction; otherwise it increases in the clockwise direction. Required - only when directionVectorAz and directionVectorEl are used to provide - the coordinates of the direction vector. - - Parameter - --------- - azccw_flag: bool - Flag indicating how azimuth is measured. - - """ - self.__azccwFlag = val - - @parameter - def elplsz_flag(self, val): - """Flag indicating how elevation is measured. - - If elplszFlag is true, elevation increases from the XY plane toward - +Z; otherwise toward -Z. Required only when directionVectorAz and - directionVectorEl are used to provide the coordinates of the direction - vector. - - Parameter - --------- - elplsz_flag: bool - ag indicating how elevation is measured. - - """ - self.__elplszFlag = val diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index aa113ac..785aaa9 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -74,12 +74,9 @@ class AngularSeparation(Calculation): Parameters ---------- - shape_1: str, optional (`TWO_TARGETS` mode) - See: :py:attr:`shape_1` - shape_2: str, optional (`TWO_TARGETS` mode) - See: :py:attr:`shape_2` - aberration_correction: str, optional - See: :py:attr:`aberration_correction` + spec_type: str, optional + See: :py:attr:`spec_type` either `'TWO_TARGETS'` (default) + or `'TWO_DIRECTIONS'`. Other Parameters ---------------- @@ -99,20 +96,29 @@ class AngularSeparation(Calculation): See: :py:attr:`time_system` time_format: str See: :py:attr:`time_format` - spec_type: str, optional - See: :py:attr:`spec_type` - target_1: str or int (`TWO_TARGETS` mode) + + `TWO_TARGETS` parameters + ------------------------ + target_1: str or int See: :py:attr:`target_1` - target_2: str or int (`TWO_TARGETS` mode) + shape_1: str, optional + See: :py:attr:`shape_1` + target_2: str or int See: :py:attr:`target_2` - observer: str or int (`TWO_TARGETS` mode) + shape_2: str, optional + See: :py:attr:`shape_2` + observer: str or int See: :py:attr:`observer` - direction_1: dict (`TWO_DIRECTIONS` mode) + aberration_correction: str, optional + See: :py:attr:`aberration_correction` + + `TWO_DIRECTIONS` parameters + --------------------------- + direction_1: Direction See: :py:attr:`direction_1` - direction_2: dict (`TWO_DIRECTIONS` mode) + direction_2: Direction See: :py:attr:`direction_2` - Raises ------ CalculationRequiredAttr @@ -142,6 +148,10 @@ def __init__(self, spec_type='TWO_TARGETS', if spec_type == 'TWO_DIRECTIONS': kwargs['spec_type'] = spec_type + # FIXME self._required('shape') in both directions + + # FIXME: VECTOR type != INSTRUMENT_FOV_BOUNDARY_VECTORS + # (only for PointingDirection calculations) kwargs['aberration_correction'] = aberration_correction diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 3aec341..178b27f 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -289,47 +289,47 @@ def cli_state_vector(argv=None): def cli_angular_separation(argv=None): - """Submit angular separation calcultion with the CLI.""" + """Submit angular separation calculation with the CLI.""" cli_calculation(argv, AngularSeparation, desc='Angular Separation') def cli_angular_size(argv=None): - """Submit angular size calcultion with the CLI.""" + """Submit angular size calculation with the CLI.""" cli_calculation(argv, AngularSize, desc='Angular Size') def cli_frame_transformation(argv=None): - """Submit frame transformation calcultion with the CLI.""" + """Submit frame transformation calculation with the CLI.""" cli_calculation(argv, FrameTransformation, desc='Frame Transformation') def cli_illumination_angles(argv=None): - """Submit illumination angles calcultion with the CLI.""" + """Submit illumination angles calculation with the CLI.""" cli_calculation(argv, IlluminationAngles, desc='Illumination Angles') def cli_subsolar_point(argv=None): - """Submit sub-solar point calcultion with the CLI.""" + """Submit sub-solar point calculation with the CLI.""" cli_calculation(argv, SubSolarPoint, desc='Sub-Solar Point') def cli_subobserver_point(argv=None): - """Submit sub-observer point calcultion with the CLI.""" + """Submit sub-observer point calculation with the CLI.""" cli_calculation(argv, SubObserverPoint, desc='Sub-Observer Point') def cli_surface_intercept_point(argv=None): - """Submit surface intercept point calcultion with the CLI.""" + """Submit surface intercept point calculation with the CLI.""" cli_calculation(argv, SurfaceInterceptPoint, desc='Surface Intercept Point') def cli_osculating_elements(argv=None): - """Submit osculating elements calcultion with the CLI.""" + """Submit osculating elements calculation with the CLI.""" cli_calculation(argv, OsculatingElements, desc='Osculating Elements') def cli_time_conversion(argv=None): - """Submit time conversion calcultion with the CLI.""" + """Submit time conversion calculation with the CLI.""" cli_calculation(argv, TimeConversion, desc='Time Conversion') diff --git a/webgeocalc/direction.py b/webgeocalc/direction.py new file mode 100644 index 0000000..b83355f --- /dev/null +++ b/webgeocalc/direction.py @@ -0,0 +1,633 @@ +"""Webgeocalc Directions.""" + +from .decorator import parameter +from .errors import CalculationInvalidAttr, CalculationIncompatibleAttr +from .payload import Payload +from .vars import VALID_PARAMETERS + + +class Direction(Payload): + """Webgeocalc direction object. + + Parameters + ---------- + aberration_correction: str, optional + See: :py:attr:`aberration_correction` (default: ``'NONE'``) + anti_vector_flag: str or bool, optional + See: :py:attr:`anti_vector_flag` (default: ``False``) + + Other Parameters + ---------------- + direction_type: str + See: :py:attr:`direction_type` + Depending on the desired :py:attr:`direction_type`, + different parameters are required. + observer: str or int + See: :py:attr:`observer` + Required if :py:attr:`aberration_correction` is not ``'NONE'`` + for direction vector of type ``'VECTOR'``. + + `POSITION` parameters + --------------------- + target: str or int + See: :py:attr:`target` + shape: str + See: :py:attr:`shape` + + `VELOCITY` parameters + --------------------- + target: str or int + See: :py:attr:`target` + reference_frame: str, optional + See: :py:attr:`reference_frame` + + `VECTOR` parameters + --------------------- + direction_vector_type: str + See: :py:attr:`direction_vector_type` + direction_instrument: str or int + See: :py:attr:`direction_instrument` + direction_frame: str + See: :py:attr:`direction_frame` + direction_frame_axis: str + See: :py:attr:`direction_frame_axis` + direction_vector_x: float + See: :py:attr:`direction_vector_x` + direction_vector_y: float + See: :py:attr:`direction_vector_y` + direction_vector_z: float + See: :py:attr:`direction_vector_z` + direction_vector_ra: float + 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` + + """ + + REQUIRED = ('direction_type', ) + + def __init__(self, aberration_correction='NONE', + anti_vector_flag=False, **kwargs): + + match kwargs.get('direction_type'): + case 'POSITION': + self.REQUIRED += ('target', 'observer') + case 'VELOCITY': + self.REQUIRED += ('target', 'reference_frame', 'observer') + case 'VECTOR': + self.REQUIRED += ('direction_vector_type',) + + kwargs['aberration_correction'] = aberration_correction + kwargs['anti_vector_flag'] = anti_vector_flag + + super().__init__(**kwargs) + + @parameter + def aberration_correction(self, val): + """SPICE aberration correction. + + Parameters + ---------- + aberration_correction: str + The SPICE aberration correction string. + + For ``POSITION`` or ``VELOCITY``, one of: + + - NONE + - LT + - LT+S + - CN + - CN+S + - XLT + - XLT+S + - XCN + - XCN+S + + For ``VECTOR``, light time correction is applied + to the rotation from the vector frame to J2000, + while stellar aberration corrections apply + to the vector direction. One of: + + - NONE + - LT + - CN + - XLT + - XCN + - S + - XS + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationRequiredAttr + If this ``DIRECTION_TYPE`` is ``VECTOR`` and + ``ABERRATION_CORRECTION`` is ``NONE`` + but :py:attr:`observer` is not provided. + + """ + match self.params['direction_type']: + case 'VECTOR': + valid = 'ABERRATION_CORRECTION_VECTOR' + if val != 'NONE': + self._required('observer') + case _: + valid = 'ABERRATION_CORRECTION' + + if val not in VALID_PARAMETERS[valid]: + raise CalculationInvalidAttr( + 'aberration_correction', + val, + VALID_PARAMETERS[valid], + ) + + self.__aberrationCorrection = val + + @parameter(only='BOOLEAN') + def anti_vector_flag(self, val): + """Anti-vector flag. + + Parameters + ---------- + anti_vector_flag: bool + `True` if the anti-vector shall be used for the direction, and `False` + otherwise. + + In type ``POSITION``, required when the target shape is `POINT` (default). + If provided when the target shape is `SPHERE`, it must be set to false, + i.e., using anti-vector direction is not supported for target bodies + modeled as spheres. + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationInvalidAttr + If this ``DIRECTION_TYPE`` is ``POSITION`` and + ``SHAPE`` is ``SPHERE`` but :py:attr:`observer` is not provided. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + if self.params['direction_type'] == 'POSITION': + if self.params.get('shape') == 'SPHERE' and val: + raise CalculationInvalidAttr( + 'anti_vector_flag', val, ['False']) + + self.__antiVectorFlag = val + + @parameter(only='DIRECTION_TYPE') + def direction_type(self, val): + """Type of direction. + + Method used to specify a direction. Directions could be specified as the + position of an object as seen from the observer, as the velocity vector of + an object as seen from the observer in a given reference frame, or by + providing a vector in a given reference frame. + + Parameters + ---------- + direction_type: str + The type of direction string. One of: + + - POSITION + - VELOCITY + - VECTOR + + Velocity depends on the reference frame in which it is expressed. + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__directionType = val + + @parameter + def observer(self, val): + """Observing body. + + Parameters + ---------- + observer: str or int + The observing body ``name`` or ``id`` from :py:func:`API.bodies`. + Required if :py:attr:`aberration_correction` is not ``'NONE'`` + for direction vector of type ``'VECTOR'``. + + """ + self.__observer = val if isinstance(val, int) else val.upper() + + @parameter + def target(self, val): + """Target body. + + Parameters + ---------- + target: str or int + The target body ``name`` or ``id`` from :py:func:`API.bodies`. + + """ + self.__target = val if isinstance(val, int) else val.upper() + + @parameter(only='TARGET_SHAPE') + def shape(self, val): + """The shape to use for the first body. + + Parameters + ---------- + shape: str + One of: + - POINT + - SPHERE + + Raises + ------- + CalculationInvalidAttr + If the value provided is invalid. + + """ + self.__shape = val + + @parameter + def reference_frame(self, val): + """The reference frame name. + + Parameters + ---------- + reference_frame: str + The reference frame name. + + """ + self.__referenceFrame = val.upper() + + @parameter(only='DIRECTION_VECTOR_TYPE') + def direction_vector_type(self, val): + """Direction vector type. + + Parameters + ---------- + direction_vector_type: str + The direction vector type string. One of: + + - INSTRUMENT_BORESIGHT + - REFERENCE_FRAME_AXIS + - VECTOR_IN_INSTRUMENT_FOV + - VECTOR_IN_REFERENCE_FRAME + - INSTRUMENT_FOV_BOUNDARY_VECTORS (*) + + (*) only for PointingDirection calculation + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationRequiredAttr + If this parameter is ``INSTRUMENT_BORESIGHT``, + ``VECTOR_IN_INSTRUMENT_FOV`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` + but :py:attr:`direction_instrument` is not provided. + CalculationRequiredAttr + If this parameter is ``REFERENCE_FRAME_AXIS`` or ``VECTOR_IN_REFERENCE_FRAME`` + but :py:attr:`direction_frame` is not provided. + CalculationRequiredAttr + If this parameter is ``REFERENCE_FRAME_AXIS`` but + :py:attr:`direction_frame_axis` is not provided. + + """ + match val: + case ( + 'INSTRUMENT_BORESIGHT' | + 'VECTOR_IN_INSTRUMENT_FOV' | + 'INSTRUMENT_FOV_BOUNDARY_VECTORS' + ): + self._required('direction_instrument') + case 'REFERENCE_FRAME_AXIS': + self._required('direction_frame', 'direction_frame_axis') + case 'VECTOR_IN_REFERENCE_FRAME': + self._required('direction_frame') + + self.__directionVectorType = val + + @parameter + def direction_instrument(self, val): + """The instrument name or ID. + + Required only if directionVectorType is `INSTRUMENT_BORESIGHT`, + `VECTOR_IN_INSTRUMENT_FOV` or `INSTRUMENT_FOV_BOUNDARY_VECTORS`. + + Parameters + ---------- + direction_instrument: str or int + The instrument ``name`` or ``ID``. + + Raises + ------ + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` not in ``INSTRUMENT_BORESIGHT``, + ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` or ``VECTOR_IN_INSTRUMENT_FOV``. + + + """ + only = [ + 'INSTRUMENT_BORESIGHT', + 'INSTRUMENT_FOV_BOUNDARY_VECTORS', + 'VECTOR_IN_INSTRUMENT_FOV', + ] + + if self.params['direction_vector_type'] not in only: + raise CalculationIncompatibleAttr( + 'direction_instrument', val, 'direction_vector_type', + self.params['direction_vector_type'], only) + + self.__directionInstrument = val if isinstance(val, int) else val.upper() + + @parameter + def direction_frame(self, val): + """The vector's reference frame name. + + Required only if directionVectorType is REFERENCE_FRAME_AXIS + or VECTOR_IN_REFERENCE_FRAME. + + Parameters: + ----------- + direction_frame: str + The vector's reference frame name. + + Raises + ------ + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` is not in ``REFERENCE_FRAME_AXIS`` + or ``VECTOR_IN_REFERENCE_FRAME``. + + """ + only = [ + 'REFERENCE_FRAME_AXIS', + 'VECTOR_IN_REFERENCE_FRAME', + ] + + if self.params['direction_vector_type'] not in only: + raise CalculationIncompatibleAttr( + 'direction_instrument', val, 'direction_vector_type', + self.params['direction_vector_type'], only) + + self.__directionFrame = val + + @parameter(only='AXIS') + def direction_frame_axis(self, val): + """The direction vector frame axis. + + Required only if directionVectorType is REFERENCE_FRAME_AXIS. + + Parameters + ---------- + direction_frame_axis: str + The direction frame axis string. One of: + + - X + - Y + - Z + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` is not ``REFERENCE_FRAME_AXIS``. + CalculationInvalidAttr + If the value provided is invalid. + + """ + if self.params['direction_vector_type'] != 'REFERENCE_FRAME_AXIS': + raise CalculationIncompatibleAttr( + 'direction_frame_axis', val, 'direction_vector_type', + self.params['direction_vector_type'], ['REFERENCE_FRAME_AXIS']) + + self.__directionFrameAxis = val + + def _vector(self, axis, val): + """Direction vector coordinate. + + Parameters + ---------- + axis: str + Axis name. + val: float + Value on the axis. + + Raises + ------ + CalculationIncompatibleAttr + If :py:attr:`direction_vector_type` is not in ``VECTOR_IN_INSTRUMENT_FOV`` + or ``VECTOR_IN_REFERENCE_FRAME``. + + """ + only = [ + 'VECTOR_IN_INSTRUMENT_FOV', + 'VECTOR_IN_REFERENCE_FRAME', + ] + + if self.params['direction_vector_type'] not in only: + raise CalculationIncompatibleAttr( + 'direction_vector_' + axis, val, 'direction_vector_type', + self.params['direction_vector_type'], only) + + return val + + @parameter + def direction_vector_x(self, val): + """The X direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_x: float + The X direction vector coordinate value. + + """ + self.__directionVectorX = self._vector('x', val) + + @parameter + def direction_vector_y(self, val): + """The Y direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_y: float + The Y direction vector coordinate value. + + """ + self.__directionVectorY = self._vector('y', val) + + @parameter + def direction_vector_z(self, val): + """The Z direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_z: float + The Z direction vector coordinate value. + + """ + self.__directionVectorZ = self._vector('z', val) + + @parameter + def direction_vector_ra(self, val): + """The right ascension direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_ra: float + The right ascension direction vector coordinate value. + + """ + self.__directionVectorRA = self._vector('ra', val) + + @parameter + def direction_vector_dec(self, val): + """The declination direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_dec: float + The declination direction vector coordinate value. + + """ + self.__directionVectorDec = self._vector('dec', val) + + @parameter + def direction_vector_az(self, val): + """The azimuth direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_az: float + The azimuth direction vector coordinate value. + + """ + self._required('azccwFlag') + self.__directionVectorAz = self._vector('az', val) + + @parameter + def direction_vector_el(self, val): + """The elevation direction vector coordinate. + + If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or + VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, + directionVectorY, and directionVectorZ must be provided, or both + directionVectorRA and directionVectorDec, or both directionVectorAz + and directionVectorEl. + + Parameter + --------- + direction_vector_el: float + The elevation vector coordinate value. + + """ + self._required('elplszFlag') + self.__directionVectorEl = self._vector('el', val) + + @parameter(only='BOOLEAN') + def azccw_flag(self, val): + """Flag indicating how azimuth is measured. + + If azccwFlag is ``true``, azimuth increases in the counterclockwise + direction; otherwise it increases in the clockwise direction. Required + only when directionVectorAz and directionVectorEl are used to provide + the coordinates of the direction vector. + + Parameter + --------- + azccw_flag: bool + Flag indicating how azimuth is measured. + + Raises + ------ + CalculationUndefinedAttr + If :py:attr:`direction_vector_az` is not provided. + CalculationInvalidAttr + If the value provided is invalid. + + """ + if 'direction_vector_az' not in self.params: + raise CalculationUndefinedAttr( + 'azccw_flag', val, 'direction_vector_az') + + 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 elplszFlag is true, elevation increases from the XY plane toward + +Z; otherwise toward -Z. Required only when directionVectorAz and + directionVectorEl are used to provide the coordinates of the direction + vector. + + Parameter + --------- + elplsz_flag: bool + ag indicating how elevation is measured. + + Raises + ------ + CalculationUndefinedAttr + If :py:attr:`direction_vector_el` is not provided. + CalculationInvalidAttr + If the value provided is invalid. + + """ + if 'direction_vector_el' not in self.params: + raise CalculationUndefinedAttr( + 'elplsz_flag', val, 'direction_vector_el') + + if isinstance(val, str): + val = val.upper() == 'TRUE' + + self.__elplszFlag = val diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 317fda6..b21adbf 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -110,11 +110,6 @@ 'TWO_TARGETS', 'TWO_DIRECTIONS', ], - 'DIRECTION_TYPE': [ - 'POSITION', - 'VELOCITY', - 'VECTOR', - ], 'STATE_REPRESENTATION': [ 'RECTANGULAR', 'RA_DEC', @@ -180,13 +175,6 @@ 'NADIR/DSK/UNPRIORITIZED', 'INTERCEPT/DSK/UNPRIORITIZED', ], - 'DIRECTION_VECTOR_TYPE': [ - 'INSTRUMENT_BORESIGHT', - 'INSTRUMENT_FOV_BOUNDARY_VECTORS', - 'REFERENCE_FRAME_AXIS', - 'VECTOR_IN_INSTRUMENT_FOV', - 'VECTOR_IN_REFERENCE_FRAME', - ], 'INTERVAL_ADJUSTMENT': [ 'NO_ADJUSTMENT', 'EXPAND_INTERVALS', @@ -229,4 +217,26 @@ 'LOCMAX', 'LOCMIN', ], + 'DIRECTION_TYPE': [ + 'POSITION', + 'VELOCITY', + 'VECTOR', + ], + 'BOOLEAN': [ + True, + False, + 'true', + 'false', + 'True', + 'False', + 'TRUE', + 'FALSE', + ], + 'DIRECTION_VECTOR_TYPE': [ + 'INSTRUMENT_BORESIGHT', + 'REFERENCE_FRAME_AXIS', + 'VECTOR_IN_INSTRUMENT_FOV', + 'VECTOR_IN_REFERENCE_FRAME', + 'INSTRUMENT_FOV_BOUNDARY_VECTORS', + ], } From 87ef975c2e5fa9e28e334072d2913cd391998f72 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Wed, 19 Mar 2025 14:19:47 +0100 Subject: [PATCH 20/25] Add missing Direction tests --- .pylintrc | 2 +- setup.cfg | 4 +- tests/test_direction.py | 601 +++++++++++++++++++++++--------------- webgeocalc/calculation.py | 6 +- webgeocalc/direction.py | 42 ++- webgeocalc/errors.py | 4 +- 6 files changed, 417 insertions(+), 242 deletions(-) diff --git a/.pylintrc b/.pylintrc index d44e0a8..2442412 100644 --- a/.pylintrc +++ b/.pylintrc @@ -571,4 +571,4 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "Exception". -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/setup.cfg b/setup.cfg index f8e0d19..dbae5d7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,11 +17,11 @@ addopts = --verbose [coverage:report] show_missing = True fail_under = 100 -exclude_lines = +exclude_lines = def __repr__ [flake8] max-line-length = 90 -ignore = D105, D107, D401 +ignore = D105, D107, D401, W504 max-complexity = 10 exclude = build, dist, venv, *.egg-info diff --git a/tests/test_direction.py b/tests/test_direction.py index eb57b30..7ec80d7 100644 --- a/tests/test_direction.py +++ b/tests/test_direction.py @@ -1,10 +1,12 @@ """Test WGC direction setup.""" -import pytest -from pytest import fixture, raises +import re + +from pytest import raises from webgeocalc.direction import Direction -from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr +from webgeocalc.errors import (CalculationIncompatibleAttr, CalculationInvalidAttr, + CalculationRequiredAttr, CalculationUndefinedAttr) def test_direction_position_with_shape(): @@ -23,6 +25,7 @@ def test_direction_position_with_shape(): 'antiVectorFlag': False, } + def test_direction_position_without_shape(): """Test direction position type without shape (for `PointingDirection`).""" assert Direction( @@ -41,7 +44,7 @@ def test_direction_position_without_shape(): def test_direction_position_errors(): - """Test direction position type errors """ + """Test direction position type errors.""" # Missing target with raises(CalculationRequiredAttr): _ = Direction( @@ -73,7 +76,7 @@ def test_direction_position_errors(): target='MARS', shape='SPHERE', observer='EARTH', - aberration_correction='S', + aberration_correction='S', # only for direction VECTOR ) # Invalid anti vector flag when shape is sphere @@ -106,8 +109,8 @@ def test_direction_velocity(): } -def test_direction_position_errors(): - """Test direction position type errors """ +def test_direction_velocity_errors(): + """Test direction velocity type errors.""" # Missing target with raises(CalculationRequiredAttr): _ = Direction( @@ -133,8 +136,8 @@ def test_direction_position_errors(): ) -def test_direction_vector_inst_boresight(): - """Test direction vector for instrument boresight.""" +def test_direction_vector_instrument_boresight(): + """Test direction vector with instrument boresight.""" # Without observer (aberration correction NONE) assert Direction( direction_type='VECTOR', @@ -165,8 +168,8 @@ def test_direction_vector_inst_boresight(): } -def test_direction_vector_ref_frame_axis(): - """Test direction vector for reference fame axis.""" +def test_direction_vector_frame_axis(): + """Test direction vector with fame axis.""" assert Direction( direction_type='VECTOR', observer='EARTH', @@ -186,254 +189,392 @@ def test_direction_vector_ref_frame_axis(): } -def test_direction_vector_in_instr_fov(): - """Test direction vector for in instrument FOV.""" +def test_direction_vector_in_instrument_fov_xyz(): + """Test direction vector with in instrument FOV (X/Y/Z).""" assert Direction( direction_type='VECTOR', direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', - direction_instrument='CASSINI_RPWS_EDIPOLE', - direction_frame_axis='Z', + direction_instrument='CASSINI_ISS_NAC', + direction_vector_x=0, + direction_vector_y=0, + direction_vector_z=1, ).payload == { 'directionType': 'VECTOR', 'directionVectorType': 'VECTOR_IN_INSTRUMENT_FOV', - 'directionInstrument': 'CASSINI_RPWS_EDIPOLE', - 'directionFrameAxis': 'Z', + 'directionInstrument': 'CASSINI_ISS_NAC', + 'directionVectorX': 0, + 'directionVectorY': 0, + 'directionVectorZ': 1, + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + } + + +def test_direction_vector_in_reference_frame_radec(): + """Test direction vector with in reference fame (RA/DEC).""" + assert Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_ra=0, + direction_vector_dec=45, + ).payload == { + 'directionType': 'VECTOR', + 'directionVectorType': 'VECTOR_IN_REFERENCE_FRAME', + 'directionFrame': 'J2000', + 'directionVectorRA': 0, + 'directionVectorDec': 45, + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + } + + +def test_direction_vector_in_reference_frame_azel(): + """Test direction vector with in reference fame (AZ/EL).""" + assert Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + direction_vector_el=15, + azccw_flag=True, + elplsz_flag='True', + ).payload == { + 'directionType': 'VECTOR', + 'directionVectorType': 'VECTOR_IN_REFERENCE_FRAME', + 'directionFrame': 'DSS-13_TOPO', + 'directionVectorAz': 5, + 'directionVectorEl': 15, + "azccwFlag": True, + "elplszFlag": True, 'aberrationCorrection': 'NONE', 'antiVectorFlag': False, } -def test_direction_vector_errors(): - """Test direction vector errors.""" +def test_direction_vector_instrument_fov_boundary_vectors(): + """Test direction vector with instrument FOV boundary vectors.""" + assert Direction( + direction_type='VECTOR', + observer='CASSINI', + 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', + 'directionInstrument': 'CASSINI_ISS_NAC', + 'aberrationCorrection': 'CN', + 'antiVectorFlag': False, + } + + +def test_direction_vector_requirements_errors(): + """Test direction vector requirements errors.""" # Missing direction_vector_type with raises(CalculationRequiredAttr): _ = Direction( - direction_type='VECTOR', + direction_type='VECTOR', + ) + + # Missing direction_instrument (with INSTRUMENT_BORESIGHT) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + ) + + # Missing direction_instrument (with VECTOR_IN_INSTRUMENT_FOV) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + ) + + # Missing direction_instrument (with INSTRUMENT_FOV_BOUNDARY_VECTORS) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_FOV_BOUNDARY_VECTORS', + ) + + # Missing direction_frame (with REFERENCE_FRAME_AXIS) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + ) + + # Missing direction_frame (with VECTOR_IN_REFERENCE_FRAME) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + ) + + # Missing direction_frame_axis (with REFERENCE_FRAME_AXIS) + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='IAU_EARTH', + ) + + +def test_direction_vector_coordinates_errors(): + """Test direction vector coordinates errors.""" + err = re.escape( + "Attribute 'direction_vector_type' is set " + "to 'VECTOR_IN_INSTRUMENT_FOV' " + "but 'direction_vector_x/y/z' or " + "'direction_vector_ra/dec' or " + "'direction_vector_az/el' attribute is undefined." ) + # Missing direction_vector_x + with raises(CalculationUndefinedAttr, match=err): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + direction_instrument='CASSINI_ISS_NAC', + direction_vector_y=0, + direction_vector_z=1, + ) + + # Missing direction_vector_y + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + direction_instrument='CASSINI_ISS_NAC', + direction_vector_x=0, + direction_vector_z=1, + ) + + # Missing direction_vector_z + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + direction_instrument='CASSINI_ISS_NAC', + direction_vector_x=0, + direction_vector_y=0, + ) + # Missing direction_vector_ra + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_dec=45, + ) + + # Missing direction_vector_dec + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_ra=0, + ) + + # Missing direction_vector_az + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_el=15, + azccw_flag=True, + elplsz_flag=True, + ) + + # Missing direction_vector_el + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + azccw_flag=True, + elplsz_flag=True, + ) + + # Missing azccw_flag + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + direction_vector_el=15, + elplsz_flag=True, + ) + + # Missing elplsz_flag + with raises(CalculationRequiredAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + direction_vector_el=15, + azccw_flag=True, + ) + + # Missing direction_vector_az with azccw_flag + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + azccw_flag=True, + ) + + # Missing direction_vector_el with elplsz_flag + with raises(CalculationUndefinedAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + elplsz_flag=True, + ) + + +def test_direction_vector_abcorr_errors(): + """Test direction vector aberration correction errors.""" # Missing observer with aberration correction NONE with raises(CalculationRequiredAttr): _ = Direction( - direction_type='VECTOR', - direction_vector_type='INSTRUMENT_BORESIGHT', - direction_instrument='CASSINI_ISS_NAC', - aberration_correction='CN', - ) + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + aberration_correction='CN', + ) - # - -## ---- Remove below FIXME ----- - - -# @fixture -# def direction_vector_inst_boresight(): -# """Input vector direction.""" -# return { -# "direction_type": "VECTOR", -# "direction_vector_type": "INSTRUMENT_BORESIGHT", -# "direction_instrument": "CASSINI_ISS_NAC", -# } - - -# @fixture -# def direction_vector_inst_boresight_payload(): -# """Vector direction payload.""" -# return { -# 'aberrationCorrection': 'NONE', -# 'antiVectorFlag': False, -# "directionType": "VECTOR", -# "directionVectorType": "INSTRUMENT_BORESIGHT", -# "directionInstrument": "CASSINI_ISS_NAC", -# } - - -# @fixture -# def direction_vector_ref_frame_axis(): -# """Input vector direction.""" -# return { -# "direction_type": "VECTOR", -# "direction_vector_type": "REFERENCE_FRAME_AXIS", -# "direction_frame": "CASSINI_RPWS_EDIPOLE", -# "direction_frame_axis": "Z" -# } - - -# @fixture -# def direction_vector_ref_frame_axis_payload(): -# """Vector direction payload.""" -# return { -# 'aberrationCorrection': 'NONE', -# 'antiVectorFlag': False, -# "directionType": "VECTOR", -# "directionVectorType": "REFERENCE_FRAME_AXIS", -# "directionFrame": "CASSINI_RPWS_EDIPOLE", -# "directionFrameAxis": "Z" -# } - - -@fixture -def direction_vector_in_ref_frame_xyz(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_x": 0.0, - "direction_vector_y": 0.0, - "direction_vector_z": 1.0, - } + # Invalid with aberration correction with VECTOR + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='VECTOR', + observer='CASSINI', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + aberration_correction='CN+S', # +S invalid for direction VECTOR + ) -@fixture -def direction_vector_in_ref_frame_xyz_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorX": 0.0, - "directionVectorY": 0.0, - "directionVectorZ": 1.0, - } +def test_direction_vector_invalid_errors(): + """Test direction vector invalid errors.""" + # Invalid direction_vector_type + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='WRONG', + ) + # Invalid direction_frame_axis + with raises(CalculationInvalidAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='IAU_EARTH', + direction_frame_axis='W', + ) -@fixture -def direction_vector_in_ref_frame_radec(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_ra": 0.0, - "direction_vector_dec": 0.0, - } + # Invalid azccw_flag + with raises(CalculationInvalidAttr): + assert Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + direction_vector_el=15, + azccw_flag='WRONG', + elplsz_flag=True, + ) + # Invalid elplsz_flag + with raises(CalculationInvalidAttr): + assert Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='DSS-13_TOPO', + direction_vector_az=5, + direction_vector_el=15, + azccw_flag='TRUE', + elplsz_flag='WRONG', + ) -@fixture -def direction_vector_in_ref_frame_radec_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorRA": 0.0, - "directionVectorDec": 0.0, - } +def test_direction_vector_incompatible_errors(): + """Test direction vector incompatibility errors.""" + # Incompatible direction_instrument (with REFERENCE_FRAME_AXIS) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='IAU_EARTH', + direction_frame_axis='X', + direction_instrument='INCOMPATIBLE', + ) -@fixture -def direction_vector_in_ref_frame_azel(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "VECTOR_IN_REFERENCE_FRAME", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_vector_az": 0.0, - "direction_vector_el": 0.0, - "azccw_flag": True, - "elplsz_flag": True, - } + # Incompatible direction_instrument (with VECTOR_IN_REFERENCE_FRAME) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_REFERENCE_FRAME', + direction_frame='J2000', + direction_vector_ra=0, + direction_vector_dec=45, + direction_instrument='INCOMPATIBLE', + ) + # Incompatible direction_frame (with INSTRUMENT_BORESIGHT) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + direction_frame='INCOMPATIBLE', + ) -@fixture -def direction_vector_in_ref_frame_azel_payload(): - """Vector direction payload.""" - return { - 'aberrationCorrection': 'NONE', - 'antiVectorFlag': False, - "directionType": "VECTOR", - "directionVectorType": "VECTOR_IN_REFERENCE_FRAME", - "directionFrame": "CASSINI_RPWS_EDIPOLE", - "directionVectorAz": 0.0, - "directionVectorEl": 0.0, - "azccwFlag": True, - "elplszFlag": True, - } + # Incompatible direction_frame (with VECTOR_IN_INSTRUMENT_FOV) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='VECTOR_IN_INSTRUMENT_FOV', + direction_instrument='CASSINI_ISS_NAC', + direction_frame='INCOMPATIBLE', + direction_vector_x=0, + direction_vector_y=0, + direction_vector_z=1, + ) + # Incompatible direction_frame (with INSTRUMENT_FOV_BOUNDARY_VECTORS) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_FOV_BOUNDARY_VECTORS', + direction_instrument='CASSINI_ISS_NAC', + direction_frame='INCOMPATIBLE', + ) -# @fixture -# def direction_velocity(): -# """Input velocity direction.""" -# return { -# "directionType": "VELOCITY", -# "target": "CASSINI", -# "referenceFrame": "IAU_SATURN", -# "observer": "SATURN" -# } - - -# @fixture -# def direction_velocity_payload(): -# """Vector direction payload.""" -# return { -# 'aberrationCorrection': 'NONE', -# 'antiVectorFlag': False, -# "directionType": "VELOCITY", -# "target": "CASSINI", -# "referenceFrame": "IAU_SATURN", -# "observer": "SATURN" -# } - - -# @fixture -# def direction_position(): -# """Input position direction.""" -# 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_position_anti_vector_flag_invalid(): -# """Input position direction.""" -# return { -# "directionType": "POSITION", -# "target": "SUN", -# "shape": "POINT", -# "observer": "CASSINI", -# "antiVectorFlag": "Test" -# } - - -@pytest.mark.parametrize("fixture_name", [ - # "direction_vector_inst_boresight", - # "direction_vector_ref_frame_axis", - "direction_vector_in_ref_frame_xyz", - "direction_vector_in_ref_frame_radec", - "direction_vector_in_ref_frame_azel", - # "direction_velocity", - # "direction_position", -]) -def test_direction_vector_in_ref_frame(request, fixture_name): - """Test direction payload.""" - params = request.getfixturevalue(fixture_name) - payload = request.getfixturevalue(fixture_name + '_payload') - assert Direction(**params).payload == payload - - -# def test_direction_position_anti_vector_flag_invalid_error( -# direction_position_anti_vector_flag_invalid -# ): -# """Test anti-vector flag error.""" -# with raises(CalculationInvalidAttr): -# Direction(**direction_position_anti_vector_flag_invalid) + # Incompatible direction_frame_axis (without REFERENCE_FRAME_AXIS) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + direction_frame_axis='X', + ) + + # Incompatible direction_vector_x|y|z|ra|dec|az|el + # (without VECTOR_IN_INSTRUMENT_FOV or VECTOR_IN_REFERENCE_FRAME) + with raises(CalculationIncompatibleAttr): + _ = Direction( + direction_type='VECTOR', + direction_vector_type='INSTRUMENT_BORESIGHT', + direction_instrument='CASSINI_ISS_NAC', + direction_vector_x=0, + ) diff --git a/webgeocalc/calculation.py b/webgeocalc/calculation.py index cdeac36..99cadbb 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -1482,11 +1482,11 @@ def direction_vector_type(self, val): keys = self.params.keys() if val in ['VECTOR_IN_INSTRUMENT_FOV', 'VECTOR_IN_REFERENCE_FRAME']: if not ( - 'direction_vector_x' in keys and # noqa: W504 - 'direction_vector_y' in keys and # noqa: W504 + 'direction_vector_x' in keys and + 'direction_vector_y' in keys and 'direction_vector_z' in keys ) and not ( - 'direction_vector_ra' in keys and # noqa: W504 + 'direction_vector_ra' in keys and 'direction_vector_dec' in keys ): raise CalculationUndefinedAttr( diff --git a/webgeocalc/direction.py b/webgeocalc/direction.py index b83355f..c824259 100644 --- a/webgeocalc/direction.py +++ b/webgeocalc/direction.py @@ -1,7 +1,8 @@ """Webgeocalc Directions.""" from .decorator import parameter -from .errors import CalculationInvalidAttr, CalculationIncompatibleAttr +from .errors import (CalculationIncompatibleAttr, CalculationInvalidAttr, + CalculationUndefinedAttr) from .payload import Payload from .vars import VALID_PARAMETERS @@ -179,9 +180,10 @@ def anti_vector_flag(self, val): val = val.upper() == 'TRUE' if self.params['direction_type'] == 'POSITION': - if self.params.get('shape') == 'SPHERE' and val: + if self.params.get('shape') == 'SPHERE' and val: raise CalculationInvalidAttr( - 'anti_vector_flag', val, ['False']) + 'anti_vector_flag', val, ['False'] + ) self.__antiVectorFlag = val @@ -315,8 +317,38 @@ def direction_vector_type(self, val): case 'VECTOR_IN_REFERENCE_FRAME': self._required('direction_frame') + match val: + case ( + 'VECTOR_IN_INSTRUMENT_FOV' | + 'VECTOR_IN_REFERENCE_FRAME' + ): + if not self._vector_coordinates(): + raise CalculationUndefinedAttr( + 'direction_vector_type', val, + "' or '".join([ + 'direction_vector_x/y/z', + 'direction_vector_ra/dec', + 'direction_vector_az/el', + ]) + ) + self.__directionVectorType = val + def _vector_coordinates(self): + """Check if the vector any coordinates are present.""" + keys = self.params.keys() + return ( + 'direction_vector_x' in keys and + 'direction_vector_y' in keys and + 'direction_vector_z' in keys + ) or ( + 'direction_vector_ra' in keys and + 'direction_vector_dec' in keys + ) or ( + 'direction_vector_az' in keys and + 'direction_vector_el' in keys + ) + @parameter def direction_instrument(self, val): """The instrument name or ID. @@ -548,7 +580,7 @@ def direction_vector_az(self, val): The azimuth direction vector coordinate value. """ - self._required('azccwFlag') + self._required('azccw_flag') self.__directionVectorAz = self._vector('az', val) @parameter @@ -567,7 +599,7 @@ def direction_vector_el(self, val): The elevation vector coordinate value. """ - self._required('elplszFlag') + self._required('elplsz_flag') self.__directionVectorEl = self._vector('el', val) @parameter(only='BOOLEAN') diff --git a/webgeocalc/errors.py b/webgeocalc/errors.py index 1dea7c8..fdde819 100644 --- a/webgeocalc/errors.py +++ b/webgeocalc/errors.py @@ -60,7 +60,9 @@ class CalculationInvalidAttr(AttributeError): def __init__(self, name, attr, valids): msg = '\n - '.join( - [f"Attribute '{name}'='{attr}' is only applicable with:"] + valids) + [f"Attribute '{name}'='{attr}' is only applicable with:"] + + [str(v) for v in valids] + ) super().__init__(msg) From dbbb642d62f8c5379bcfce858e7ac298a7287837 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Thu, 20 Mar 2025 11:59:12 +0100 Subject: [PATCH 21/25] Update Calculation cross-refs attrs in docs --- docs/calculation.rst | 254 +++++++++++++++++++++---------------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/docs/calculation.rst b/docs/calculation.rst index f87eb6b..3029e21 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -45,13 +45,13 @@ All WebGeoCalc calculation objects take their input attributes in .. important:: Calculation required parameters: - - :py:attr:`.calculation_type` - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` + - :py:attr:`~Calculation.calculation_type` + - :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` Calculation default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` .. note:: @@ -102,7 +102,7 @@ All WebGeoCalc calculation objects take their input attributes in The payload that will be submitted to the WebGeoCalc API can be retrieve with the -:py:attr:`.payload` attribute: +:py:attr:`~Calculation.payload` attribute: >>> calc.payload {'kernels': [{'type': 'KERNEL_SET', 'id': 5}], @@ -116,10 +116,10 @@ the WebGeoCalc API can be retrieve with the 'timeSystem': 'UTC', 'timeFormat': 'CALENDAR'} -Example of :py:class:`StateVector` calculation with multi :py:attr:`.kernels` -inputs (requested by ``name`` in this case), with multiple :py:attr:`.times` -inputs for :py:attr:`.target`, :py:attr:`.observer` and -:py:attr:`.reference_frame` requested by ``id``: +Example of :py:class:`StateVector` calculation with multi :py:attr:`~Calculation.kernels` +inputs (requested by ``name`` in this case), with multiple :py:attr:`~Calculation.times` +inputs for :py:attr:`~Calculation.target`, :py:attr:`~Calculation.observer` and +:py:attr:`~Calculation.reference_frame` requested by ``id``: >>> StateVector( ... kernels = ['Solar System Kernels', 'Cassini Huygens'], @@ -142,7 +142,7 @@ inputs for :py:attr:`.target`, :py:attr:`.observer` and Example of :py:class:`AngularSeparation` calculation -with specific :py:attr:`.kernel_paths` and multiple :py:attr:`.intervals`: +with specific :py:attr:`~Calculation.kernel_paths` and multiple :py:attr:`~Calculation.intervals`: >>> AngularSeparation( ... kernel_paths = [ @@ -197,7 +197,7 @@ calculation status with :py:func:`Calculation.update` method: [Calculation update] Status: COMPLETE (id: 8750344d-645d-4e43-b159-c8d88d28aac6) 3. When the calculation status is `COMPLETE`, the -results are retrieved by the :py:attr:`.results` attribute: +results are retrieved by the :py:attr:`~Calculation.results` attribute: >>> calc.results # doctest: +SKIP { @@ -320,17 +320,17 @@ calculated in a desired reference frame: .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.observer` - - :py:attr:`.reference_frame` + - :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` + - :py:attr:`~Calculation.reference_frame` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.aberration_correction`: ``CN`` - - :py:attr:`.state_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.state_representation`: ``RECTANGULAR`` .. autoclass:: StateVector @@ -380,20 +380,20 @@ The second case is the angular separation between two directions .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target_1` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`.target_2` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`.observer` if :py:attr:`.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`.direction_1` if :py:attr:`.spec_type` is `TWO_DIRECTIONS` - - :py:attr:`.direction_2` if :py:attr:`.spec_type` is `TWO_DIRECTIONS` + - :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_1` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`~Calculation.target_2` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`~Calculation.observer` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set + - :py:attr:`~Calculation.direction_1` if :py:attr:`~Calculation.spec_type` is `TWO_DIRECTIONS` + - :py:attr:`~Calculation.direction_2` if :py:attr:`~Calculation.spec_type` is `TWO_DIRECTIONS` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.shape_1`: ``POINT`` - - :py:attr:`.shape_2`: ``POINT`` - - :py:attr:`.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.shape_1`: ``POINT`` + - :py:attr:`~Calculation.shape_2`: ``POINT`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` .. autoclass:: AngularSeparation @@ -420,15 +420,15 @@ Calculates the angular size of a target as seen by an observer. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.observer` + - :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:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` .. autoclass:: AngularSize @@ -463,23 +463,23 @@ another reference frame (Frame 2). .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.frame_1` - - :py:attr:`.frame_2` + - :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.frame_1` + - :py:attr:`~Calculation.frame_2` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.aberration_correction`: ``CN`` - - :py:attr:`.time_location`: ``FRAME1``, - - :py:attr:`.orientation_representation`: ``EULER_ANGLES``, - - :py:attr:`.axis_1`: ``X``, - - :py:attr:`.axis_2`: ``Y``, - - :py:attr:`.axis_3`: ``Z``, - - :py:attr:`.angular_units`: ``deg``, - - :py:attr:`.angular_velocity_representation`: ``VECTOR_IN_FRAME1``, - - :py:attr:`.angular_velocity_units`: ``deg/s``' + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.time_location`: ``FRAME1``, + - :py:attr:`~Calculation.orientation_representation`: ``EULER_ANGLES``, + - :py:attr:`~Calculation.axis_1`: ``X``, + - :py:attr:`~Calculation.axis_2`: ``Y``, + - :py:attr:`~Calculation.axis_3`: ``Z``, + - :py:attr:`~Calculation.angular_units`: ``deg``, + - :py:attr:`~Calculation.angular_velocity_representation`: ``VECTOR_IN_FRAME1``, + - :py:attr:`~Calculation.angular_velocity_units`: ``deg/s``' .. autoclass:: FrameTransformation @@ -517,20 +517,20 @@ target as seen from an observer. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.target_frame` - - :py:attr:`.observer` - - :py:attr:`.latitude` - - :py:attr:`.longitude` + - :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.latitude` + - :py:attr:`~Calculation.longitude` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.shape_1`: ``ELLIPSOID`` - - :py:attr:`.coordinate_representation`: ``LATITUDINAL`` - - :py:attr:`.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.shape_1`: ``ELLIPSOID`` + - :py:attr:`~Calculation.coordinate_representation`: ``LATITUDINAL`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` .. autoclass:: IlluminationAngles @@ -568,18 +568,18 @@ Calculates the sub-solar point on a target as seen from an observer. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.target_frame` - - :py:attr:`.observer` + - :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` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.sub_point_type`: ``Near point: ellipsoid`` - - :py:attr:`.aberration_correction`: ``CN`` - - :py:attr:`.state_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.sub_point_type`: ``Near point: ellipsoid`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.state_representation`: ``RECTANGULAR`` .. autoclass:: SubSolarPoint @@ -618,18 +618,18 @@ Calculate the sub-observer point on a target as seen from an observer. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.target_frame` - - :py:attr:`.observer` + - :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` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.sub_point_type`: ``Near point: ellipsoid`` - - :py:attr:`.aberration_correction`: ``CN`` - - :py:attr:`.state_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.sub_point_type`: ``Near point: ellipsoid`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.state_representation`: ``RECTANGULAR`` .. autoclass:: SubObserverPoint @@ -671,19 +671,19 @@ from an observer. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.target` - - :py:attr:`.target_frame` - - :py:attr:`.observer` + - :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` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` - :py:attr:`shape_1`: ``ELLIPSOID`` - - :py:attr:`.intercept_vector_type`: ``INSTRUMENT_BORESIGHT`` - - :py:attr:`.aberration_correction`: ``CN`` - - :py:attr:`.state_representation`: ``RECTANGULAR`` + - :py:attr:`~Calculation.intercept_vector_type`: ``INSTRUMENT_BORESIGHT`` + - :py:attr:`~Calculation.aberration_correction`: ``CN`` + - :py:attr:`~Calculation.state_representation`: ``RECTANGULAR`` .. autoclass:: SurfaceInterceptPoint @@ -721,15 +721,15 @@ central body. The orbit may be elliptical, parabolic, or hyperbolic. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.orbiting_body` - - :py:attr:`.center_body` + - :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.orbiting_body` + - :py:attr:`~Calculation.center_body` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.reference_frame`: ``J2000`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.reference_frame`: ``J2000`` .. autoclass:: OsculatingElements @@ -756,14 +756,14 @@ Convert times from one time system or format to another. .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` + - :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` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.output_time_system`: ``UTC`` - - :py:attr:`.output_time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.output_time_system`: ``UTC`` + - :py:attr:`~Calculation.output_time_format`: ``CALENDAR`` .. autoclass:: TimeConversion @@ -804,24 +804,24 @@ Find time intervals when a coordinate of an observer-target position vector sati .. important:: Calculation required parameters: - - :py:attr:`.kernels` or/and :py:attr:`.kernel_paths` - - :py:attr:`.times` or :py:attr:`.intervals` with :py:attr:`.time_step` and :py:attr:`.time_step_units` - - :py:attr:`.observer` - - :py:attr:`.target` - - :py:attr:`.reference_frame` - - :py:attr:`.coordinate_system` - - :py:attr:`.coordinate` - - :py:attr:`.relational_condition` - - :py:attr:`.reference_value` only if :py:attr:`.relational_condition` is not ``ABSMAX``, ``ABSMIN``, ``LOCMAX``, or ``LOCMIN`` - - :py:attr:`.upper_limit` only if :py:attr:`.relational_condition` is ``RANGE`` - - :py:attr:`.adjustment_value` only if :py:attr:`.relational_condition` is ``ABSMAX`` or ``ABSMIN`` + - :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.observer` + - :py:attr:`~Calculation.target` + - :py:attr:`~Calculation.reference_frame` + - :py:attr:`~Calculation.coordinate_system` + - :py:attr:`~Calculation.coordinate` + - :py:attr:`~Calculation.relational_condition` + - :py:attr:`~Calculation.reference_value` only if :py:attr:`~Calculation.relational_condition` is not ``ABSMAX``, ``ABSMIN``, ``LOCMAX``, or ``LOCMIN`` + - :py:attr:`~Calculation.upper_limit` only if :py:attr:`~Calculation.relational_condition` is ``RANGE`` + - :py:attr:`~Calculation.adjustment_value` only if :py:attr:`~Calculation.relational_condition` is ``ABSMAX`` or ``ABSMIN`` Default parameters: - - :py:attr:`.time_system`: ``UTC`` - - :py:attr:`.time_format`: ``CALENDAR`` - - :py:attr:`.output_duration_units`: ``SECONDS`` - - :py:attr:`.should_complement_window`: ``False`` - - :py:attr:`.interval_adjustment`: ``NO_ADJUSTMENT`` - - :py:attr:`.interval_filtering`: ``NO_FILTERING`` + - :py:attr:`~Calculation.time_system`: ``UTC`` + - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + - :py:attr:`~Calculation.output_duration_units`: ``SECONDS`` + - :py:attr:`~Calculation.should_complement_window`: ``False`` + - :py:attr:`~Calculation.interval_adjustment`: ``NO_ADJUSTMENT`` + - :py:attr:`~Calculation.interval_filtering`: ``NO_FILTERING`` .. autoclass:: GFCoordinateSearch From fec869b900f794ce05f0ad7a60925525d5ddb609 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Thu, 20 Mar 2025 14:37:24 +0100 Subject: [PATCH 22/25] Add Direction and Payload documentations --- docs/index.rst | 1 + docs/params.rst | 131 +++++++++++++ webgeocalc/direction.py | 396 ++++++++++++++++++++-------------------- webgeocalc/payload.py | 19 +- 4 files changed, 340 insertions(+), 207 deletions(-) create mode 100644 docs/params.rst diff --git a/docs/index.rst b/docs/index.rst index 0dd59b2..e642d10 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -95,6 +95,7 @@ Documentation api calculation + params cli .. important:: diff --git a/docs/params.rst b/docs/params.rst new file mode 100644 index 0000000..ee735df --- /dev/null +++ b/docs/params.rst @@ -0,0 +1,131 @@ +Advanced parameters +=================== + +Payload +-------- + +.. currentmodule:: webgeocalc.payload + +WebGeoCalc API requires JSON encoded payloads. An abstract class :py:class:`Payload` is +available to convert any python keywords values pattern into a structure dictionary that +can be encoded into JSON. It also provided a mechanism to enforce some required keywords +and restrict some parameter to a subset of ``VALID_PARAMETERS`` +(see :py:mod:`webgeocalc.vars`). + +>>> from webgeocalc.payload import Payload +>>> from webgeocalc.decorator import parameter + +>>> class DerivedPayload(Payload): +... REQUIRED = ('foo',) +... +... @parameter +... def foo(self, val): # required +... self.__foo = val +... +... @parameter(only='AXIS') # optional +... def baz(self, val): +... self.__baz = val + + +>>> DerivedPayload(foo='bar', baz='X').payload +{'foo': 'bar', 'baz': 'X'} + +.. autoclass:: webgeocalc.payload.Payload + +Direction +--------- + +.. currentmodule:: webgeocalc.direction + +Direction vectors for ``ANGULAR_SEPARATION`` and ``POINTING_DIRECTION`` can be specified as +an explicit :py:type:`dict` but it is recommended to use an explicit with :py:class:`Direction` +object: + +>>> from webgeocalc.direction import Direction + +>>> Direction( +... direction_type='POSITION', +... target='MARS', +... shape='POINT', +... observer='EARTH', +... aberration_correction='LT+S', +... ).payload +{'directionType': 'POSITION', + 'target': 'MARS', + 'shape': 'POINT', + 'observer': 'EARTH', + 'aberrationCorrection': 'LT+S', + 'antiVectorFlag': False} + +>>> Direction( +... direction_type='VELOCITY', +... target='MARS', +... reference_frame='ITRF93', +... observer='EARTH', +... aberration_correction='XCN+S', +... anti_vector_flag=True, +... ).payload +{'directionType': 'VELOCITY', + 'target': 'MARS', + 'referenceFrame': 'ITRF93', + 'observer': 'EARTH', + 'aberrationCorrection': 'XCN+S', + 'antiVectorFlag': True} + +>>> Direction( +... direction_type='VECTOR', +... observer='EARTH', +... direction_vector_type='REFERENCE_FRAME_AXIS', +... direction_frame='IAU_EARTH', +... direction_frame_axis='X', +... aberration_correction='S', +... anti_vector_flag=True, +... ).payload +{'directionType': 'VECTOR', + 'observer': 'EARTH', + 'directionVectorType': 'REFERENCE_FRAME_AXIS', + 'directionFrame': 'IAU_EARTH', + 'directionFrameAxis': 'X', + 'aberrationCorrection': 'S', + 'antiVectorFlag': True} + +.. important:: + + Direction required parameters: + - :py:attr:`~Direction.direction_type` either ``POSITION``, ``VELOCITY`` or ``VECTOR`` + - :py:attr:`~Direction.observer` (not required if :py:attr:`aberration_correction` is ``NONE`` + and :py:attr:`~Direction.direction vector` is ``VECTOR``) + + Direction ``POSITION`` required parameters: + - :py:attr:`~Direction.target` + - :py:attr:`~Direction.shape` + + Direction ``VELOCITY`` required parameters: + - :py:attr:`~Direction.target` + - :py:attr:`~Direction.reference_frame` + + Direction ``VECTOR`` required parameters: + - :py:attr:`~Direction.direction_vector_type` either + ``INSTRUMENT_BORESIGHT``, ``REFERENCE_FRAME_AXIS``, ``VECTOR_IN_INSTRUMENT_FOV``, + ``VECTOR_IN_REFERENCE_FRAME`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` + + Direction ``VECTOR + INSTRUMENT_BORESIGHT`` required parameters: + - :py:attr:`~Direction.direction_instrument` + + 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: + - :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`, + + Default parameters: + - :py:attr:`~Direction.aberration_correction`: ``NONE`` + - :py:attr:`~Direction.anti_vector_flag`: ``False`` + + +.. autoclass:: Direction diff --git a/webgeocalc/direction.py b/webgeocalc/direction.py index c824259..285a1dc 100644 --- a/webgeocalc/direction.py +++ b/webgeocalc/direction.py @@ -12,38 +12,31 @@ class Direction(Payload): Parameters ---------- - aberration_correction: str, optional - See: :py:attr:`aberration_correction` (default: ``'NONE'``) - anti_vector_flag: str or bool, optional - See: :py:attr:`anti_vector_flag` (default: ``False``) - - Other Parameters - ---------------- direction_type: str See: :py:attr:`direction_type` Depending on the desired :py:attr:`direction_type`, different parameters are required. observer: str or int See: :py:attr:`observer` - Required if :py:attr:`aberration_correction` is not ``'NONE'`` - for direction vector of type ``'VECTOR'``. + Not required if :py:attr:`aberration_correction` is ``NONE`` + and :py:attr:`direction vector` is ``VECTOR``. + + Required parameters 'POSITION' - `POSITION` parameters - --------------------- target: str or int See: :py:attr:`target` shape: str See: :py:attr:`shape` - `VELOCITY` parameters - --------------------- + Required parameters 'VELOCITY' + target: str or int See: :py:attr:`target` reference_frame: str, optional See: :py:attr:`reference_frame` - `VECTOR` parameters - --------------------- + Required parameters 'VECTOR' + direction_vector_type: str See: :py:attr:`direction_vector_type` direction_instrument: str or int @@ -71,6 +64,14 @@ class Direction(Payload): elplsz_flag: bool or str See: :py:attr:`elplsz_flag` + Other Parameters + ---------------- + aberration_correction: str, optional + See: :py:attr:`aberration_correction` (default: ``NONE``) + anti_vector_flag: str or bool, optional + See: :py:attr:`anti_vector_flag` (default: ``False``) + + """ REQUIRED = ('direction_type', ) @@ -91,102 +92,6 @@ def __init__(self, aberration_correction='NONE', super().__init__(**kwargs) - @parameter - def aberration_correction(self, val): - """SPICE aberration correction. - - Parameters - ---------- - aberration_correction: str - The SPICE aberration correction string. - - For ``POSITION`` or ``VELOCITY``, one of: - - - NONE - - LT - - LT+S - - CN - - CN+S - - XLT - - XLT+S - - XCN - - XCN+S - - For ``VECTOR``, light time correction is applied - to the rotation from the vector frame to J2000, - while stellar aberration corrections apply - to the vector direction. One of: - - - NONE - - LT - - CN - - XLT - - XCN - - S - - XS - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - CalculationRequiredAttr - If this ``DIRECTION_TYPE`` is ``VECTOR`` and - ``ABERRATION_CORRECTION`` is ``NONE`` - but :py:attr:`observer` is not provided. - - """ - match self.params['direction_type']: - case 'VECTOR': - valid = 'ABERRATION_CORRECTION_VECTOR' - if val != 'NONE': - self._required('observer') - case _: - valid = 'ABERRATION_CORRECTION' - - if val not in VALID_PARAMETERS[valid]: - raise CalculationInvalidAttr( - 'aberration_correction', - val, - VALID_PARAMETERS[valid], - ) - - self.__aberrationCorrection = val - - @parameter(only='BOOLEAN') - def anti_vector_flag(self, val): - """Anti-vector flag. - - Parameters - ---------- - anti_vector_flag: bool - `True` if the anti-vector shall be used for the direction, and `False` - otherwise. - - In type ``POSITION``, required when the target shape is `POINT` (default). - If provided when the target shape is `SPHERE`, it must be set to false, - i.e., using anti-vector direction is not supported for target bodies - modeled as spheres. - - Raises - ------ - CalculationInvalidAttr - If the value provided is invalid. - CalculationInvalidAttr - If this ``DIRECTION_TYPE`` is ``POSITION`` and - ``SHAPE`` is ``SPHERE`` but :py:attr:`observer` is not provided. - - """ - if isinstance(val, str): - val = val.upper() == 'TRUE' - - if self.params['direction_type'] == 'POSITION': - if self.params.get('shape') == 'SPHERE' and val: - raise CalculationInvalidAttr( - 'anti_vector_flag', val, ['False'] - ) - - self.__antiVectorFlag = val - @parameter(only='DIRECTION_TYPE') def direction_type(self, val): """Type of direction. @@ -201,11 +106,11 @@ def direction_type(self, val): direction_type: str The type of direction string. One of: - - POSITION - - VELOCITY - - VECTOR + - ``POSITION`` + - ``VELOCITY`` + - ``VECTOR`` - Velocity depends on the reference frame in which it is expressed. + Velocity depends on the reference frame in which it is expressed. Raises ------ @@ -249,8 +154,9 @@ def shape(self, val): ---------- shape: str One of: - - POINT - - SPHERE + + - ``POINT`` + - ``SPHERE`` Raises ------- @@ -281,13 +187,13 @@ def direction_vector_type(self, val): direction_vector_type: str The direction vector type string. One of: - - INSTRUMENT_BORESIGHT - - REFERENCE_FRAME_AXIS - - VECTOR_IN_INSTRUMENT_FOV - - VECTOR_IN_REFERENCE_FRAME - - INSTRUMENT_FOV_BOUNDARY_VECTORS (*) + - ``INSTRUMENT_BORESIGHT`` + - ``REFERENCE_FRAME_AXIS`` + - ``VECTOR_IN_INSTRUMENT_FOV`` + - ``VECTOR_IN_REFERENCE_FRAME`` + - ``INSTRUMENT_FOV_BOUNDARY_VECTORS`` (*) - (*) only for PointingDirection calculation + (*) only for :py:class:`PointingDirection` calculation. Raises ------ @@ -351,10 +257,10 @@ def _vector_coordinates(self): @parameter def direction_instrument(self, val): - """The instrument name or ID. + """The instrument direction. - Required only if directionVectorType is `INSTRUMENT_BORESIGHT`, - `VECTOR_IN_INSTRUMENT_FOV` or `INSTRUMENT_FOV_BOUNDARY_VECTORS`. + Required only if :py:attr:`direction_vector_type` is ``INSTRUMENT_BORESIGHT``, + ``VECTOR_IN_INSTRUMENT_FOV`` or ``INSTRUMENT_FOV_BOUNDARY_VECTORS``. Parameters ---------- @@ -386,11 +292,11 @@ def direction_instrument(self, val): def direction_frame(self, val): """The vector's reference frame name. - Required only if directionVectorType is REFERENCE_FRAME_AXIS - or VECTOR_IN_REFERENCE_FRAME. + Required only if :py:attr:`direction_vector_type` is ``REFERENCE_FRAME_AXIS`` + or ``VECTOR_IN_REFERENCE_FRAME``. - Parameters: - ----------- + Parameters + ---------- direction_frame: str The vector's reference frame name. @@ -417,16 +323,16 @@ def direction_frame(self, val): def direction_frame_axis(self, val): """The direction vector frame axis. - Required only if directionVectorType is REFERENCE_FRAME_AXIS. + Required only if :py:attr:`direction_vector_type` is ``REFERENCE_FRAME_AXIS``. Parameters ---------- direction_frame_axis: str The direction frame axis string. One of: - - X - - Y - - Z + - ``X`` + - ``Y`` + - ``Z`` Raises ------ @@ -478,14 +384,14 @@ def _vector(self, axis, val): def direction_vector_x(self, val): """The X direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_x`, + :py:attr:`direction_vector_y`, and + :py:attr:`direction_vector_z` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_x: float The X direction vector coordinate value. @@ -496,14 +402,14 @@ def direction_vector_x(self, val): def direction_vector_y(self, val): """The Y direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_x`, + :py:attr:`direction_vector_y`, and + :py:attr:`direction_vector_z` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_y: float The Y direction vector coordinate value. @@ -514,14 +420,14 @@ def direction_vector_y(self, val): def direction_vector_z(self, val): """The Z direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_x`, + :py:attr:`direction_vector_y`, and + :py:attr:`direction_vector_z` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_z: float The Z direction vector coordinate value. @@ -532,14 +438,13 @@ def direction_vector_z(self, val): def direction_vector_ra(self, val): """The right ascension direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_ra` and + :py:attr:`direction_vector_dec` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_ra: float The right ascension direction vector coordinate value. @@ -550,14 +455,13 @@ def direction_vector_ra(self, val): def direction_vector_dec(self, val): """The declination direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_ra` and + :py:attr:`direction_vector_dec` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_dec: float The declination direction vector coordinate value. @@ -568,14 +472,15 @@ def direction_vector_dec(self, val): def direction_vector_az(self, val): """The azimuth direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_az`, + :py:attr:`direction_vector_el`, + :py:attr:`azccw_flag` and + :py:attr:`elplsz_flag` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_az: float The azimuth direction vector coordinate value. @@ -587,14 +492,15 @@ def direction_vector_az(self, val): def direction_vector_el(self, val): """The elevation direction vector coordinate. - If directionVectorType is VECTOR_IN_INSTRUMENT_FOV or - VECTOR_IN_REFERENCE_FRAME, then either all three of directionVectorX, - directionVectorY, and directionVectorZ must be provided, or both - directionVectorRA and directionVectorDec, or both directionVectorAz - and directionVectorEl. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_az`, + :py:attr:`direction_vector_el`, + :py:attr:`azccw_flag` and + :py:attr:`elplsz_flag` must be provided. - Parameter - --------- + Parameters + ---------- direction_vector_el: float The elevation vector coordinate value. @@ -606,13 +512,15 @@ def direction_vector_el(self, val): def azccw_flag(self, val): """Flag indicating how azimuth is measured. - If azccwFlag is ``true``, azimuth increases in the counterclockwise - direction; otherwise it increases in the clockwise direction. Required - only when directionVectorAz and directionVectorEl are used to provide - the coordinates of the direction vector. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_az`, + :py:attr:`direction_vector_el`, + :py:attr:`azccw_flag` and + :py:attr:`elplsz_flag` must be provided. - Parameter - --------- + Parameters + ---------- azccw_flag: bool Flag indicating how azimuth is measured. @@ -637,13 +545,15 @@ def azccw_flag(self, val): def elplsz_flag(self, val): """Flag indicating how elevation is measured. - If elplszFlag is true, elevation increases from the XY plane toward - +Z; otherwise toward -Z. Required only when directionVectorAz and - directionVectorEl are used to provide the coordinates of the direction - vector. + If :py:attr:`direction_vector_type` is ``VECTOR_IN_INSTRUMENT_FOV`` or + ``VECTOR_IN_REFERENCE_FRAME``, then either all three of + :py:attr:`direction_vector_az`, + :py:attr:`direction_vector_el`, + :py:attr:`azccw_flag` and + :py:attr:`elplsz_flag` must be provided. - Parameter - --------- + Parameters + ---------- elplsz_flag: bool ag indicating how elevation is measured. @@ -663,3 +573,99 @@ def elplsz_flag(self, val): val = val.upper() == 'TRUE' self.__elplszFlag = val + + @parameter + def aberration_correction(self, val): + """SPICE aberration correction. + + Parameters + ---------- + aberration_correction: str + The SPICE aberration correction string. + + For ``POSITION`` or ``VELOCITY``, one of: + + - ``NONE`` + - ``LT`` + - ``LT+S`` + - ``CN`` + - ``CN+S`` + - ``XLT`` + - ``XLT+S`` + - ``XCN`` + - ``XCN+S`` + + For ``VECTOR``, light time correction is applied + to the rotation from the vector frame to ``J2000``, + while stellar aberration corrections apply + to the vector direction. One of: + + - ``NONE`` + - ``LT`` + - ``CN`` + - ``XLT`` + - ``XCN`` + - ``S`` + - ``XS`` + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationRequiredAttr + If this ``DIRECTION_TYPE`` is ``VECTOR`` and + ``ABERRATION_CORRECTION`` is ``NONE`` + but :py:attr:`observer` is not provided. + + """ + match self.params['direction_type']: + case 'VECTOR': + valid = 'ABERRATION_CORRECTION_VECTOR' + if val != 'NONE': + self._required('observer') + case _: + valid = 'ABERRATION_CORRECTION' + + if val not in VALID_PARAMETERS[valid]: + raise CalculationInvalidAttr( + 'aberration_correction', + val, + VALID_PARAMETERS[valid], + ) + + self.__aberrationCorrection = val + + @parameter(only='BOOLEAN') + def anti_vector_flag(self, val): + """Anti-vector flag. + + Parameters + ---------- + anti_vector_flag: bool + `True` if the anti-vector shall be used for the direction, and `False` + otherwise. + + In type ``POSITION``, required when the target shape is `POINT` (default). + If provided when the target shape is `SPHERE`, it must be set to false, + i.e., using anti-vector direction is not supported for target bodies + modeled as spheres. + + Raises + ------ + CalculationInvalidAttr + If the value provided is invalid. + CalculationInvalidAttr + If this :py:attr:`direction_type` is ``POSITION`` and + ``SHAPE`` is ``SPHERE`` but :py:attr:`observer` is not provided. + + """ + if isinstance(val, str): + val = val.upper() == 'TRUE' + + if self.params['direction_type'] == 'POSITION': + if self.params.get('shape') == 'SPHERE' and val: + raise CalculationInvalidAttr( + 'anti_vector_flag', val, ['False'] + ) + + self.__antiVectorFlag = val diff --git a/webgeocalc/payload.py b/webgeocalc/payload.py index 6d9cd23..2dcf65f 100644 --- a/webgeocalc/payload.py +++ b/webgeocalc/payload.py @@ -1,10 +1,12 @@ -"""WebGeoCalc Payload.""" +"""WebGeoCalc Payload submodule.""" + +from abc import ABC from .errors import CalculationRequiredAttr -class Payload: - """Abstract WebGeoCalc payload class. +class Payload(ABC): + """Abstract WebGeoCalc payload abstract class. Check if any required parameters is missing. @@ -52,19 +54,12 @@ def _required(self, *attrs): def payload(self) -> dict: """Payload parameters *dict* for JSON input in WebGeoCalc format. - Collect all the properties prefixed with `__`. + Collect all the properties prefixed with ``__*``. Return ------ - dict + dict: Payload keys and values. - Example - ------- - >>> Payload( - ... foo = 'bar', - ... ).payload # noqa: E501 - {'foo': 'bar'} - """ return dict(self) From 24889035baaadeac1b8745806445f5b96f86b297 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 09:35:19 +0100 Subject: [PATCH 23/25] Reorganize AngularSeparation Calculation logic and update docs --- docs/calculation.rst | 69 ++++++++++++++++++++++-------- tests/test_angular_separation.py | 66 +++++++++++++++------------- tests/test_frame_transformation.py | 2 +- webgeocalc/calculation_types.py | 69 +++++++++++++++--------------- 4 files changed, 123 insertions(+), 83 deletions(-) diff --git a/docs/calculation.rst b/docs/calculation.rst index 3029e21..e358f49 100644 --- a/docs/calculation.rst +++ b/docs/calculation.rst @@ -338,9 +338,9 @@ calculated in a desired reference frame: Angular Separation ------------------ -Calculates the angular separation of two bodies as seen by an -observer body. There are two types of calculation. The default -one is the angular separation between two targets (`TWO_TARGETS` +Calculates the angular separation of two bodies/directions as seen +by an observer body. There are two types of calculation. The default +one is the angular separation between two targets (``TWO_TARGETS`` mode), which is the default mode. >>> AngularSeparation( @@ -355,23 +355,23 @@ mode), which is the default mode. {'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 175.17072258} The second case is the angular separation between two directions -(`TWO_DIRECTIONS` mode). +(``TWO_DIRECTIONS`` mode). >>> AngularSeparation( -... spec_type = 'TWO_DIRECTIONS', ... kernels = 5, ... times = '2012-10-19T08:24:00.000', +... spec_type = 'TWO_DIRECTIONS', ... direction_1 = { -... "direction_type": "VECTOR", -... "direction_vector_type": "REFERENCE_FRAME_AXIS", -... "direction_frame": "CASSINI_RPWS_EDIPOLE", -... "direction_frame_axis": "Z" +... 'direction_type': 'VECTOR', +... 'direction_vector_type': 'REFERENCE_FRAME_AXIS', +... 'direction_frame': 'CASSINI_RPWS_EDIPOLE', +... 'direction_frame_axis': 'Z' ... }, ... direction_2 = { -... "direction_type": "POSITION", -... "target": "SUN", -... "shape": "POINT", -... "observer": "CASSINI" +... 'direction_type': 'POSITION', +... 'target': 'SUN', +... 'shape': 'POINT', +... 'observer': 'CASSINI' ... }, ... verbose = False, ... ).run() @@ -382,19 +382,52 @@ The second case is the angular separation between two directions 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_1` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`~Calculation.target_2` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`~Calculation.observer` if :py:attr:`~Calculation.spec_type` is `TWO_TARGETS` or not set - - :py:attr:`~Calculation.direction_1` if :py:attr:`~Calculation.spec_type` is `TWO_DIRECTIONS` - - :py:attr:`~Calculation.direction_2` if :py:attr:`~Calculation.spec_type` is `TWO_DIRECTIONS` Default parameters: + - :py:attr:`~Calculation.spec_type`: ``TWO_TARGETS`` - :py:attr:`~Calculation.time_system`: ``UTC`` - :py:attr:`~Calculation.time_format`: ``CALENDAR`` + + Additional required parameters for ``TWO_TARGETS``: + - :py:attr:`~Calculation.target_1` + - :py:attr:`~Calculation.target_2` + - :py:attr:`~Calculation.observer` + + Additional default parameters for ``TWO_TARGETS``: - :py:attr:`~Calculation.shape_1`: ``POINT`` - :py:attr:`~Calculation.shape_2`: ``POINT`` - :py:attr:`~Calculation.aberration_correction`: ``CN`` + Additional required parameters for ``TWO_DIRECTIONS``: + - :py:attr:`~Calculation.direction_1` + - :py:attr:`~Calculation.direction_2` + +.. hint:: + + The directions can be specified either with an explicit :py:type:`dict` + or with a :py:class:`Direction` object: + + >>> from webgeocalc.direction import Direction + + >>> AngularSeparation( + ... kernels = 5, + ... times = '2012-10-19T08:24:00.000', + ... spec_type = 'TWO_DIRECTIONS', + ... direction_1 = Direction( + ... direction_type = 'VECTOR', + ... direction_vector_type = 'REFERENCE_FRAME_AXIS', + ... direction_frame = 'CASSINI_RPWS_EDIPOLE', + ... direction_frame_axis = 'Z', + ... ), + ... direction_2 = Direction( + ... direction_type = 'POSITION', + ... target = 'SUN', + ... shape = 'POINT', + ... observer = 'CASSINI', + ... ), + ... ) # doctest: +SKIP + + .. autoclass:: AngularSeparation diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 9f2acf3..a1612e3 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -1,8 +1,10 @@ """Test WGC angular separation calculation.""" -from pytest import fixture +from pytest import fixture, raises +from pytest import mark from webgeocalc import AngularSeparation +from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr @fixture @@ -46,7 +48,7 @@ def corr(): @fixture def params_two_targets(kernel_paths, time, target_1, target_2, observer, corr): - """Input parameters from WGC API example.""" + """Input parameters from WGC API example (TWO_TARGETS mode).""" return { 'kernel_paths': kernel_paths, 'times': time, @@ -59,7 +61,7 @@ def params_two_targets(kernel_paths, time, target_1, target_2, observer, corr): @fixture def payload_two_targets(kernel_paths, time, target_1, target_2, observer, corr): - """Payload from WGC API example.""" + """Payload from WGC API example (TWO_TARGETS mode).""" return { "kernels": [{ "type": "KERNEL", @@ -70,9 +72,7 @@ def payload_two_targets(kernel_paths, time, target_1, target_2, observer, corr): }], "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time, - ], + "times": [time], "calculationType": "ANGULAR_SEPARATION", "target1": target_1, "shape1": "POINT", @@ -140,25 +140,24 @@ def direction_vector_payload(): @fixture def params_two_directions( kernel_set, time, direction_vector, - direction_position, corr + direction_position, ): - """Input parameters from WGC API example.""" + """Input parameters for TWO_DIRECTIONS mode.""" return { 'spec_type': 'TWO_DIRECTIONS', 'kernels': kernel_set, 'times': time, 'direction_1': direction_vector, 'direction_2': direction_position, - 'aberration_correction': corr } @fixture def payload_two_directions( kernel_set, time, direction_vector_payload, - direction_position_payload, corr + direction_position_payload, ): - """Input parameters from WGC API example.""" + """Input parameters for TWO_DIRECTIONS mode.""" return { "kernels": [{ "type": "KERNEL_SET", @@ -167,29 +166,38 @@ def payload_two_directions( "specType": "TWO_DIRECTIONS", "timeSystem": "UTC", "timeFormat": "CALENDAR", - "times": [ - time, - ], + "times": [time], "calculationType": "ANGULAR_SEPARATION", "direction1": direction_vector_payload, "direction2": direction_position_payload, - "aberrationCorrection": corr } -def test_angular_separation_payload_two_targets( - params_two_targets, - payload_two_targets -): - """Test angular separation payload (``TWO_TARGETS`` mode).""" - payload = AngularSeparation(**params_two_targets).payload - assert payload == payload_two_targets +@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_payload_two_directions( - params_two_directions, - payload_two_directions -): - """Test angular separation payload (``TWO_DIRECTIONS`` mode).""" - payload = AngularSeparation(**params_two_directions).payload - assert payload == payload_two_directions + +def test_angular_separation_errors(params_two_targets, params_two_directions, + kernel_set, time): + """Test angular separation payload errors.""" + # Missing observer for TWO_TARGETS + params_two_targets.pop('observer') + with raises(CalculationRequiredAttr): + AngularSeparation(**params_two_targets) + + # Missing direction_1 for TWO_DIRECTIONS + params_two_directions.pop('direction_1') + with raises(CalculationRequiredAttr): + AngularSeparation(**params_two_directions) + + # Invalid spec_type value + with raises(CalculationInvalidAttr): + AngularSeparation(kernels=kernel_set, times=time, spec_type='WRONG') diff --git a/tests/test_frame_transformation.py b/tests/test_frame_transformation.py index a2ad72b..ed90497 100644 --- a/tests/test_frame_transformation.py +++ b/tests/test_frame_transformation.py @@ -86,7 +86,7 @@ def test_frame_transformation_attr_err(params): """Test errors when frame transformation is invalid.""" del params['aberration_correction'] with raises(CalculationInvalidAttr): - # aberration_correctin can not be '+S' + # aberration_correction can not be '+S' FrameTransformation(aberration_correction='CN+S', **params) with raises(CalculationInvalidAttr): diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index 785aaa9..894a6ca 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -68,18 +68,11 @@ class AngularSeparation(Calculation): Calculates the angular separation of two bodies as seen by an observer body. There are two types of calculation. The default one is the angular separation - between two targets (`TWO_TARGETS` mode). The second case is the angular - separation between two directions (`TWO_DIRECTIONS` mode). - + between two targets (``TWO_TARGETS`` mode). The second case is the angular + separation between two directions (``TWO_DIRECTIONS`` mode). Parameters ---------- - spec_type: str, optional - See: :py:attr:`spec_type` either `'TWO_TARGETS'` (default) - or `'TWO_DIRECTIONS'`. - - Other Parameters - ---------------- kernels: str, int, [str or/and int] See: :py:attr:`kernels` kernel_paths: str, [str] @@ -97,36 +90,42 @@ class AngularSeparation(Calculation): time_format: str See: :py:attr:`time_format` - `TWO_TARGETS` parameters - ------------------------ + Other Parameters + ---------------- + spec_type: str, optional + See: :py:attr:`spec_type` either ``TWO_TARGETS`` (default) + or ``TWO_DIRECTIONS``. + + Required parameters `TWO_TARGETS` + target_1: str or int See: :py:attr:`target_1` shape_1: str, optional - See: :py:attr:`shape_1` + See: :py:attr:`shape_1` (default: ``POINT``) target_2: str or int See: :py:attr:`target_2` shape_2: str, optional - See: :py:attr:`shape_2` + See: :py:attr:`shape_2` (default: ``POINT``) observer: str or int See: :py:attr:`observer` aberration_correction: str, optional See: :py:attr:`aberration_correction` - `TWO_DIRECTIONS` parameters - --------------------------- - direction_1: Direction + Required parameters `TWO_DIRECTIONS` + + direction_1: dict or Direction See: :py:attr:`direction_1` - direction_2: Direction + direction_2: dict or Direction See: :py:attr:`direction_2` Raises ------ CalculationRequiredAttr - If :py:attr:`spec_type` is `TWO_TARGETS` or not set: + If :py:attr:`spec_type` is ``TWO_TARGETS`` or not set: If py:attr:`target_1`, :py:attr:`target_2` - and :py:attr:`observer` are not provided. - If :py:attr:`spec_type` is `TWO_DIRECTIONS`: - If :py:attr:`direction_1` and :py:attr:`direction_2` + or :py:attr:`observer` are not provided. + If :py:attr:`spec_type` is ``TWO_DIRECTIONS``: + If :py:attr:`direction_1` or :py:attr:`direction_2` are not provided. """ @@ -136,24 +135,24 @@ def __init__(self, spec_type='TWO_TARGETS', kwargs['calculation_type'] = 'ANGULAR_SEPARATION' - spec_type_required = { - 'TWO_TARGETS': ('target_1', 'target_2', 'observer'), - 'TWO_DIRECTIONS': ('direction_1', 'direction_2') - } - self.REQUIRED = spec_type_required[spec_type] + match spec_type: + case 'TWO_TARGETS': + self.REQUIRED += ('target_1', 'target_2', 'observer') - if spec_type == 'TWO_TARGETS': - for key in ['shape_1', 'shape_2']: - kwargs.setdefault(key, 'POINT') + for key in ['shape_1', 'shape_2']: + kwargs.setdefault(key, 'POINT') - if spec_type == 'TWO_DIRECTIONS': - kwargs['spec_type'] = spec_type - # FIXME self._required('shape') in both directions + kwargs['aberration_correction'] = aberration_correction - # FIXME: VECTOR type != INSTRUMENT_FOV_BOUNDARY_VECTORS - # (only for PointingDirection calculations) + case 'TWO_DIRECTIONS': + self.REQUIRED += ('direction_1', 'direction_2') - kwargs['aberration_correction'] = aberration_correction + kwargs['spec_type'] = spec_type + + case _: + raise CalculationInvalidAttr( + 'spec_type', spec_type, VALID_PARAMETERS['SPEC_TYPE'], + ) super().__init__(**kwargs) From 20bebb5625c11d1ba54fe91fbf9ebfc24e53c3c3 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 12:58:01 +0100 Subject: [PATCH 24/25] Update calculations notebook --- examples/calculation.ipynb | 223 +++++++++++++++++++------------------ 1 file changed, 113 insertions(+), 110 deletions(-) diff --git a/examples/calculation.ipynb b/examples/calculation.ipynb index d16c645..27187b8 100644 --- a/examples/calculation.ipynb +++ b/examples/calculation.ipynb @@ -227,23 +227,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 5f85a881-cd76-4cdc-8291-85eb25a92e3e)\n" + "[Calculation submit] Phase: COMPLETE (id: 026506ff-340c-407d-bdd6-3fc6db7b156e)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 09:00:00.000000 UTC',\n", - " 'DISTANCE': 764142.63776247,\n", - " 'SPEED': 111.54765899,\n", - " 'X': 298292.85744169,\n", - " 'Y': -651606.58468976,\n", - " 'Z': 265224.81187627,\n", - " 'D_X_DT': -98.8032491,\n", - " 'D_Y_DT': -51.73211296,\n", - " 'D_Z_DT': -2.1416539,\n", + " 'DISTANCE': 764142.65053372,\n", + " 'SPEED': 111.54765158,\n", + " 'X': 298293.06093747,\n", + " 'Y': -651606.39373107,\n", + " 'Z': 265225.08895284,\n", + " 'D_X_DT': -98.80322113,\n", + " 'D_Y_DT': -51.73215012,\n", + " 'D_Z_DT': -2.14166057,\n", " 'TIME_AT_TARGET': '2012-10-19 08:59:57.451094 UTC',\n", - " 'LIGHT_TIME': 2.54890548}" + " 'LIGHT_TIME': 2.54890552}" ] }, "execution_count": 5, @@ -269,7 +269,7 @@ "source": [ "## Angular Separation\n", "\n", - "Calculates the angular separation of two bodies as seen by an observer body: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#angular-separation))" + "Calculates the angular separation of `TWO_TARGETS` as seen by an observer body: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#angular-separation))" ] }, { @@ -281,7 +281,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: d2f28b90-f6de-47d4-b69c-8603e5e090d0)\n" + "[Calculation submit] Phase: COMPLETE (id: 18cc2eeb-8f89-40f4-bf58-6557fcf1108e)\n" ] }, { @@ -309,28 +309,38 @@ }, { "cell_type": "markdown", - "source": [ - "Calculates the angular separation of two directions: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#angular-separation)) " - ], "metadata": { "collapsed": false - } + }, + "source": [ + "Calculates the angular separation of `TWO_DIRECTIONS`: ([docs](https://webgeocalc.readthedocs.io/en/stable/calculation.html#angular-separation)) " + ] }, { "cell_type": "code", + "execution_count": 7, + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-04T13:28:44.424317Z", + "start_time": "2025-03-04T13:28:42.241069Z" + }, + "collapsed": false + }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 6d697dad-dd2b-40df-af20-bd2d97a96fbc)\n" + "[Calculation submit] Phase: COMPLETE (id: 9528a534-a5de-45f4-ae14-d2de0bf5ec55)\n" ] }, { "data": { - "text/plain": "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 90.10114616}" + "text/plain": [ + "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 90.10114616}" + ] }, - "execution_count": 1, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -355,15 +365,7 @@ " \"observer\": \"CASSINI\"\n", " },\n", ").run()" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2025-03-04T13:28:44.424317Z", - "start_time": "2025-03-04T13:28:42.241069Z" - } - }, - "execution_count": 1 + ] }, { "cell_type": "markdown", @@ -376,23 +378,23 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 810fd7c7-635d-45d6-828c-ef24e8942524)\n" + "[Calculation submit] Phase: COMPLETE (id: 1da914a8-7e61-43a1-9137-7b362cf1d269)\n" ] }, { "data": { "text/plain": [ - "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SIZE': 0.03037939}" + "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SIZE': 0.03032491}" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -420,30 +422,30 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: f81aac7c-01d2-4347-97e2-e8934ade1001)\n" + "[Calculation submit] Phase: COMPLETE (id: 047d013a-75c3-4a0a-aab9-1f0f4c9a159f)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 08:24:00.000000 UTC',\n", - " 'ANGLE3': -20.58940104,\n", - " 'ANGLE2': 0.01874004,\n", - " 'ANGLE1': 0.00136319,\n", - " 'AV_X': 9.94596495e-07,\n", - " 'AV_Y': -7.23492228e-08,\n", - " 'AV_Z': -0.00634331,\n", - " 'AV_MAG': 0.00634331}" + " 'ANGLE3': -19.59511576,\n", + " 'ANGLE2': -0.00533619,\n", + " 'ANGLE1': -0.00345332,\n", + " 'AV_X': -2.8406831e-07,\n", + " 'AV_Y': 1.83751477e-07,\n", + " 'AV_Z': -0.00633942,\n", + " 'AV_MAG': 0.00633942}" ] }, - "execution_count": 8, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -471,30 +473,30 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: a8546482-10c0-43d2-b07c-75ac1132a220)\n" + "[Calculation submit] Phase: COMPLETE (id: bad25967-14c7-4a2e-87f0-d5607e39ac2c)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 08:24:00.000000 UTC',\n", - " 'INCIDENCE_ANGLE': 24.78527742,\n", - " 'EMISSION_ANGLE': 25.56007298,\n", - " 'PHASE_ANGLE': 1.00079007,\n", - " 'OBSERVER_ALTITUDE': 967668.02765637,\n", - " 'TIME_AT_POINT': '2012-10-19 08:23:56.772207 UTC',\n", - " 'LIGHT_TIME': 3.2277931,\n", - " 'LTST': '13:15:59'}" + " 'INCIDENCE_ANGLE': 25.51886414,\n", + " 'EMISSION_ANGLE': 26.31058362,\n", + " 'PHASE_ANGLE': 1.00106425,\n", + " 'OBSERVER_ALTITUDE': 967670.28784259,\n", + " 'TIME_AT_POINT': '2012-10-19 08:23:56.772199 UTC',\n", + " 'LIGHT_TIME': 3.22780064,\n", + " 'LTST': '13:19:56'}" ] }, - "execution_count": 9, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -525,33 +527,33 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 47d473f2-77b9-4fe3-8388-d1c7d54b2e25)\n" + "[Calculation submit] Phase: COMPLETE (id: 62023c10-3bef-4736-bdf9-92b50fabc5c1)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 08:24:00.000000 UTC',\n", - " 'X': 234.00550655,\n", - " 'Y': -77.32612213,\n", - " 'Z': 67.42916937,\n", - " 'SUB_POINT_RADIUS': 255.50851089,\n", - " 'OBSERVER_ALTITUDE': 967644.15493281,\n", - " 'INCIDENCE_ANGLE': 4.49798357e-15,\n", - " 'EMISSION_ANGLE': 0.99611862,\n", - " 'PHASE_ANGLE': 0.99611862,\n", - " 'TIME_AT_POINT': '2012-10-19 08:23:56.772287 UTC',\n", - " 'LIGHT_TIME': 3.22771347}" + " 'X': 232.15437562,\n", + " 'Y': -81.18742303,\n", + " 'Z': 67.66010394,\n", + " 'SUB_POINT_RADIUS': 255.07830453,\n", + " 'OBSERVER_ALTITUDE': 967644.95641522,\n", + " 'INCIDENCE_ANGLE': 1.10177646e-14,\n", + " 'EMISSION_ANGLE': 0.99615507,\n", + " 'PHASE_ANGLE': 0.99615507,\n", + " 'TIME_AT_POINT': '2012-10-19 08:23:56.772284 UTC',\n", + " 'LIGHT_TIME': 3.22771614}" ] }, - "execution_count": 10, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -580,34 +582,34 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 07e5f27e-7372-4eb7-8405-55bcc6015d10)\n" + "[Calculation submit] Phase: COMPLETE (id: 42d674e1-c5aa-4fc7-9d87-27af8a54efa4)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 08:24:00.000000 UTC',\n", - " 'X': 232.5831733,\n", - " 'Y': -81.40386728,\n", - " 'Z': 67.35505213,\n", - " 'SUB_POINT_RADIUS': 255.45689491,\n", - " 'OBSERVER_ALTITUDE': 967644.11734179,\n", - " 'INCIDENCE_ANGLE': 0.99586304,\n", - " 'EMISSION_ANGLE': 1.66981544e-12,\n", - " 'PHASE_ANGLE': 0.99586304,\n", - " 'TIME_AT_POINT': '2012-10-19 08:23:56.772287 UTC',\n", - " 'LIGHT_TIME': 3.22771334,\n", + " 'X': 230.66149425,\n", + " 'Y': -85.24005493,\n", + " 'Z': 67.58656174,\n", + " 'SUB_POINT_RADIUS': 255.02653827,\n", + " 'OBSERVER_ALTITUDE': 967644.91882136,\n", + " 'INCIDENCE_ANGLE': 0.99589948,\n", + " 'EMISSION_ANGLE': 3.94425842e-12,\n", + " 'PHASE_ANGLE': 0.99589948,\n", + " 'TIME_AT_POINT': '2012-10-19 08:23:56.772284 UTC',\n", + " 'LIGHT_TIME': 3.22771602,\n", " 'LTST': '11:58:49'}" ] }, - "execution_count": 11, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -636,33 +638,33 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 5d9566ac-652a-4aa7-b118-42c4dff55285)\n" + "[Calculation submit] Phase: COMPLETE (id: 3a05e143-20f5-44a1-b63b-371db60aa702)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-14 00:00:00.000000 UTC',\n", - " 'LONGITUDE': 98.7675609,\n", - " 'LATITUDE': -38.69027976,\n", - " 'INTERCEPT_RADIUS': 57739.95803153,\n", - " 'OBSERVER_ALTITUDE': 1831047.67987589,\n", - " 'INCIDENCE_ANGLE': 123.05323675,\n", - " 'EMISSION_ANGLE': 5.8567773,\n", - " 'PHASE_ANGLE': 123.77530312,\n", + " 'LONGITUDE': 98.76797447,\n", + " 'LATITUDE': -38.69300277,\n", + " 'INTERCEPT_RADIUS': 57739.67660691,\n", + " 'OBSERVER_ALTITUDE': 1831047.98047459,\n", + " 'INCIDENCE_ANGLE': 123.05303919,\n", + " 'EMISSION_ANGLE': 5.8595724,\n", + " 'PHASE_ANGLE': 123.7753032,\n", " 'TIME_AT_POINT': '2012-10-14 00:00:00.000000 UTC',\n", - " 'LIGHT_TIME': 6.10771763,\n", + " 'LIGHT_TIME': 6.10771863,\n", " 'LTST': '20:03:06'}" ] }, - "execution_count": 12, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -695,33 +697,34 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 883bdd2d-3b7c-4aa2-b91e-f787ee69945f)\n" + "[Calculation submit] Phase: LOADING_KERNELS (id: 9d7686a6-4f73-4853-ab4f-1ed887f924ce)\n", + "[Calculation update] Phase: COMPLETE (id: 9d7686a6-4f73-4853-ab4f-1ed887f924ce)\n" ] }, { "data": { "text/plain": [ "{'DATE': '2012-10-19 08:24:00.000000 UTC',\n", - " 'PERIFOCAL_DISTANCE': 474789.03917271,\n", - " 'ECCENTRICITY': 0.70348463,\n", - " 'INCLINATION': 38.18727034,\n", - " 'ASCENDING_NODE_LONGITUDE': 223.98123058,\n", - " 'ARGUMENT_OF_PERIAPSE': 71.59474487,\n", - " 'MEAN_ANOMALY_AT_EPOCH': 14.65461204,\n", - " 'ORBITING_BODY_RANGE': 753794.65101401,\n", - " 'ORBITING_BODY_SPEED': 8.77222231,\n", - " 'PERIOD': 2067101.2236748,\n", + " 'PERIFOCAL_DISTANCE': 474789.01814487,\n", + " 'ECCENTRICITY': 0.70348464,\n", + " 'INCLINATION': 38.18736036,\n", + " 'ASCENDING_NODE_LONGITUDE': 223.98121958,\n", + " 'ARGUMENT_OF_PERIAPSE': 71.59475294,\n", + " 'MEAN_ANOMALY_AT_EPOCH': 14.65461277,\n", + " 'ORBITING_BODY_RANGE': 753794.66333655,\n", + " 'ORBITING_BODY_SPEED': 8.77222221,\n", + " 'PERIOD': 2067101.1993984,\n", " 'CENTER_BODY_GM': 37931207.49865224}" ] }, - "execution_count": 13, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -748,14 +751,14 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: 0ec03563-002b-4225-9218-6dbc4ff80e8d)\n" + "[Calculation submit] Phase: COMPLETE (id: a6bc5edc-b5a2-456f-817a-b5f65acaeae4)\n" ] }, { @@ -764,7 +767,7 @@ "{'DATE': '1/1729329441.004', 'DATE2': '2012-10-19 08:24:02.919085 UTC'}" ] }, - "execution_count": 14, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -792,23 +795,23 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Calculation submit] Phase: COMPLETE (id: e4ceb336-05f8-47b7-9e37-c64d8791e96f)\n" + "[Calculation submit] Phase: COMPLETE (id: 2b2b4806-13cb-43ea-882b-40e46f517722)\n" ] }, { "data": { "text/plain": [ - "{'DATE': '2012-10-19 08:39:33.812153 UTC', 'DURATION': 3394.10937738}" + "{'DATE': '2012-10-19 08:39:33.814938 UTC', 'DURATION': 3394.11539114}" ] }, - "execution_count": 15, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -841,7 +844,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "venv", "language": "python", "name": "python3" }, @@ -855,7 +858,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.13.1" } }, "nbformat": 4, From 80e6536bc28499b49a438876ab13b9a10e5edb83 Mon Sep 17 00:00:00 2001 From: Benoit Seignovert Date: Fri, 21 Mar 2025 14:31:12 +0100 Subject: [PATCH 25/25] Add TWO_DIRECTIONS to wgc-angular-separation CLI --- docs/cli.rst | 30 ++++++ tests/test_angular_separation.py | 17 ++-- tests/test_cli.py | 163 ++++++++++++++++++++++++------- webgeocalc/cli.py | 27 ++++- 4 files changed, 191 insertions(+), 46 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 420033c..d63ab3c 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -250,6 +250,36 @@ Here is the list of all the calculation entry points available on the CLI: - ``wgc-time-conversion`` - ``wgc-gf-coordinate-search`` +.. hint:: + + If you need to provide a :py:class:`~webgeocalc.direction.Direction` + (eg. an :py:class:`~webgeocalc.AngularSeparation` calculation with ``TWO_DIRECTIONS``). + You need to encapsulate the nested parameters into single (``'``) or double (``"``) quotes + separated with spaces: + + .. code:: bash + + $ wgc-angular-separation --dry-run \ + --kernels 5 \ + --times 2012-10-19T08:24:00 \ + --spec_type TWO_DIRECTIONS \ + --direction_1 "direction_type=POSITION target=SUN shape=POINT observer='CASSINI'" \ + --direction_2 'direction_type=VECTOR direction_vector_type=REFERENCE_FRAME_AXIS direction_frame="CASSINI_RPWS_EDIPOLE" direction_frame_axis=Z' + + API: https://wgc2.jpl.nasa.gov:8443/webgeocalc/api + Payload: + { + kernels: [{'type': 'KERNEL_SET', 'id': 5}], + times: ['2012-10-19T08:24:00.000'], + direction1: {'directionType': 'POSITION', 'target': 'SUN', 'shape': 'POINT', 'observer': 'CASSINI', 'aberrationCorrection': 'NONE', 'antiVectorFlag': False}, + direction2: {'directionType': 'VECTOR', 'directionVectorType': 'REFERENCE_FRAME_AXIS', 'directionFrame': 'CASSINI_RPWS_EDIPOLE', 'directionFrameAxis': 'Z', 'aberrationCorrection': 'NONE', 'antiVectorFlag': False}, + calculationType: ANGULAR_SEPARATION, + specType: TWO_DIRECTIONS, + timeSystem: UTC, + timeFormat: CALENDAR, + } + + All the calculation entry point accept an optional ``api`` attribute to submit the query to a custom endpoint. If ``WGC_URL`` global environment variable is defined, diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index a1612e3..87c6eb6 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -4,6 +4,7 @@ from pytest import mark from webgeocalc import AngularSeparation +from webgeocalc.direction import Direction from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr @@ -91,7 +92,7 @@ def kernel_set(): @fixture def direction_position(): - """Input position direction.""" + """Input position direction as dict object.""" return { "direction_type": "POSITION", "target": "SUN", @@ -115,13 +116,13 @@ def direction_position_payload(): @fixture def direction_vector(): - """Input vector direction.""" - return { - "direction_type": "VECTOR", - "direction_vector_type": "REFERENCE_FRAME_AXIS", - "direction_frame": "CASSINI_RPWS_EDIPOLE", - "direction_frame_axis": "Z" - } + """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 diff --git a/tests/test_cli.py b/tests/test_cli.py index 42ce4ea..75060b8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -154,6 +154,39 @@ def parse(x): assert parse('--kernels "Cassini" "Solar"') == {'kernels': ['Cassini', 'Solar']} assert parse("--times '2012-10-19T08:24:00'") == {'times': '2012-10-19T08:24:00'} + assert parse("--times 2000-01-01 --times 2000-01-02'") == { + 'times': ['2000-01-01', '2000-01-02'] + } + + +def test_cli_input_parameters_nested(): + """Test CLI nested input parameters parsing.""" + # Double quotes + assert _params([ + '--double-quotes', + '"direction_type=POSITION target=SUN shape=POINT observer=\'CASSINI\'"', + ]) == { + 'double_quotes': { + 'direction_type': 'POSITION', + 'target': 'SUN', + 'shape': 'POINT', + 'observer': 'CASSINI', + } + } + + # Single quote + assert _params([ + '--single-quote', + "'direction_type=VECTOR direction_vector_type=REFERENCE_FRAME_AXIS" + " direction_frame=\"CASSINI_RPWS_EDIPOLE\" direction_frame_axis=Z'", + ]) == { + 'single_quote': { + 'direction_type': 'VECTOR', + 'direction_vector_type': 'REFERENCE_FRAME_AXIS', + 'direction_frame': 'CASSINI_RPWS_EDIPOLE', + 'direction_frame_axis': 'Z', + } + } def test_cli_state_vector_empty(capsys): @@ -164,14 +197,109 @@ def test_cli_state_vector_empty(capsys): assert 'usage:' in captured.out +def test_cli_state_vector_esa(capsys): + """Test dry-run state vector calculation on ESA API with the CLI.""" + argv = [ + '--dry-run', + '--api', 'ESA', + '--kernels', '"OPS -- Rosetta"', + '--times', '2014-01-01T01:23:45.000', + '--calculation_type', 'STATE_VECTOR', + '--target', '"67P/CHURYUMOV-GERASIMENKO (1969 R1)"', + '--observer', '"ROSETTA ORBITER"', + '--reference_frame', '"67P/C-G_CK"', + '--aberration_correction', 'NONE', + '--state_representation', 'LATITUDINAL', + ] + + cli_state_vector(argv) + captured = capsys.readouterr() + assert 'API: http://spice.esac.esa.int/webgeocalc/api' in captured.out + assert 'Payload:' in captured.out + assert "kernels: [{'type': 'KERNEL_SET', 'id': 13}]" in captured.out + assert "times: ['2014-01-01T01:23:45.000']" in captured.out + assert 'target: 67P/CHURYUMOV-GERASIMENKO (1969 R1)' in captured.out + assert 'observer: ROSETTA ORBITER' in captured.out + assert 'referenceFrame: 67P/C-G_CK' in captured.out + assert 'calculationType: STATE_VECTOR' in captured.out + assert 'aberrationCorrection: NONE' in captured.out + assert 'stateRepresentation: LATITUDINAL' in captured.out + assert 'timeSystem: UTC' in captured.out + assert 'timeFormat: CALENDAR' in captured.out + + def test_cli_angular_separation_two_targets_dry_run(capsys): """Test dry-run angular separation calculation for 2 targets with the CLI.""" - assert False # FIXME + argv = ('--dry-run ' + '--kernels 5 ' + '--times 2012-10-19T08:24:00 ' + '--target_1 VENUS ' + '--target_2 MERCURY ' + '--observer SUN').split() + + cli_angular_separation(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "kernels: [{'type': 'KERNEL_SET', 'id': 5}]" in captured.out + assert "times: ['2012-10-19T08:24:00']" in captured.out + assert 'target1: VENUS' in captured.out + assert 'shape1: POINT' in captured.out + assert 'target2: MERCURY' in captured.out + assert 'shape2: POINT' in captured.out + assert 'observer: SUN' in captured.out + assert 'timeSystem: UTC' in captured.out + assert 'aberrationCorrection: CN' in captured.out + assert 'timeFormat: CALENDAR' in captured.out def test_cli_angular_separation_two_directions_dry_run(capsys): """Test dry-run angular separation calculation for 2 direction with the CLI.""" - assert False # FIXME + argv = [ + '--dry-run', + '--kernels', '5', + '--times', '2012-10-19T08:24:00', + '--spec_type', 'TWO_DIRECTIONS', + # double quotes + '--direction_1', '"direction_type=POSITION ' + 'target=SUN ' + 'shape=POINT ' + 'observer=\'CASSINI\'"', + # single quote + '--direction_2', "'direction_type=VECTOR " + "direction_vector_type=REFERENCE_FRAME_AXIS " + "direction_frame=\"CASSINI_RPWS_EDIPOLE\" " + "direction_frame_axis=Z'", + ] + + cli_angular_separation(argv) + captured = capsys.readouterr() + assert 'Payload:' in captured.out + assert "calculationType: ANGULAR_SEPARATION" in captured.out + assert "kernels: [{'type': 'KERNEL_SET', 'id': 5}]" in captured.out + assert "times: ['2012-10-19T08:24:00']" in captured.out + assert "specType: TWO_DIRECTIONS" in captured.out + assert ( + "direction1: {" + "'directionType': 'POSITION', " + "'target': 'SUN', " + "'shape': 'POINT', " + "'observer': 'CASSINI', " + "'aberrationCorrection': 'NONE', " + "'antiVectorFlag': False" + "}" + ) in captured.out + assert ( + "direction2: {" + "'directionType': 'VECTOR', " + "'directionVectorType': 'REFERENCE_FRAME_AXIS', " + "'directionFrame': 'CASSINI_RPWS_EDIPOLE', " + "'directionFrameAxis': 'Z', " + "'aberrationCorrection': 'NONE', " + "'antiVectorFlag': False" + "}" + ) in captured.out + assert 'timeSystem: UTC' in captured.out + assert 'timeFormat: CALENDAR' in captured.out def test_cli_angular_separation_wrong_attr(capsys): @@ -375,34 +503,3 @@ def test_cli_gf_coordinate_search_dry_run(capsys): "'relationalCondition': '<', " "'referenceValue': 0.25" "}") in captured.out - - -def test_cli_state_vector_esa(capsys): - """Test dry-run state vector calculation on ESA API with the CLI.""" - argv = [ - '--dry-run', - '--api', 'ESA', - '--kernels', '"OPS -- Rosetta"', - '--times', '2014-01-01T01:23:45.000', - '--calculation_type', 'STATE_VECTOR', - '--target', '"67P/CHURYUMOV-GERASIMENKO (1969 R1)"', - '--observer', '"ROSETTA ORBITER"', - '--reference_frame', '"67P/C-G_CK"', - '--aberration_correction', 'NONE', - '--state_representation', 'LATITUDINAL', - ] - - cli_state_vector(argv) - captured = capsys.readouterr() - assert 'API: http://spice.esac.esa.int/webgeocalc/api' in captured.out - assert 'Payload:' in captured.out - assert "kernels: [{'type': 'KERNEL_SET', 'id': 13}]" in captured.out - assert "times: ['2014-01-01T01:23:45.000']" in captured.out - assert 'target: 67P/CHURYUMOV-GERASIMENKO (1969 R1)' in captured.out - assert 'observer: ROSETTA ORBITER' in captured.out - assert 'referenceFrame: 67P/C-G_CK' in captured.out - assert 'calculationType: STATE_VECTOR' in captured.out - assert 'aberrationCorrection: NONE' in captured.out - assert 'stateRepresentation: LATITUDINAL' in captured.out - assert 'timeSystem: UTC' in captured.out - assert 'timeFormat: CALENDAR' in captured.out diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 178b27f..a45f040 100644 --- a/webgeocalc/cli.py +++ b/webgeocalc/cli.py @@ -170,11 +170,27 @@ def cli_instruments(argv=None): parser.print_help() -def _split(string, sep=','): +def _strip(string, chars='[]="\''): + for char in chars: + string = string.replace(char, '') + return string + + +def _strip_split(string, sep=',', strip='[]="\''): + """Strip and split string.""" + # Split inline dict (eg. `foo=bar baz=qux`) + if '=' in string and ' ' in string: + return [ + dict( + _strip(s, chars='"\'').split('=') + for s in string.split() + ) + ] + # Replace and split string - for char in ['[', ']', '=', '"', "'"]: + for char in strip: string = string.replace(char, '') - return string.split(sep) + return _strip(string, chars='[]="\'').split(sep) def _underscore_case(string): @@ -212,11 +228,12 @@ def _params(params): if key is None: continue - for value in _split(param): + for value in _strip_split(param): if value == '': continue - value = _int_float_str(value) + if not isinstance(value, dict): + value = _int_float_str(value) if key not in out: out[key] = value