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/.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/docs/calculation.rst b/docs/calculation.rst index 056b6de..e358f49 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 @@ -338,8 +338,10 @@ calculated in a desired reference frame: Angular Separation ------------------ -Calculates the angular separation of two bodies as seen by an -observer body. +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( ... kernel_paths = ['pds/wgc/kernels/lsk/naif0012.tls', @@ -352,21 +354,79 @@ 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( +... 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_2 = { +... 'direction_type': '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:`~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:`.shape_1`: ``POINT`` - - :py:attr:`.shape_2`: ``POINT`` - - :py:attr:`.aberration_correction`: ``CN`` + - :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 @@ -393,15 +453,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 @@ -436,23 +496,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 @@ -490,20 +550,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 @@ -541,18 +601,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 @@ -591,18 +651,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 @@ -644,19 +704,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 @@ -694,15 +754,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 @@ -729,14 +789,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 @@ -777,24 +837,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 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/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/examples/calculation.ipynb b/examples/calculation.ipynb index e9635e2..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" ] }, { @@ -307,6 +307,66 @@ ").run()" ] }, + { + "cell_type": "markdown", + "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: 9528a534-a5de-45f4-ae14-d2de0bf5ec55)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'DATE': '2012-10-19 08:24:00.000000 UTC', 'ANGULAR_SEPARATION': 90.10114616}" + ] + }, + "execution_count": 7, + "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", + " \"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", + " \"direction_type\": \"POSITION\",\n", + " \"target\": \"SUN\",\n", + " \"shape\": \"POINT\",\n", + " \"observer\": \"CASSINI\"\n", + " },\n", + ").run()" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -318,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" } @@ -362,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" } @@ -413,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" } @@ -467,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" } @@ -522,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" } @@ -578,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" } @@ -637,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" } @@ -690,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" ] }, { @@ -706,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" } @@ -734,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" } @@ -783,7 +844,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "venv", "language": "python", "name": "python3" }, @@ -797,7 +858,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.13.1" } }, "nbformat": 4, 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/setup.py b/setup.py index 8f20977..e3f1385 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', @@ -22,9 +22,9 @@ 'Documentation': 'https://webgeocalc.readthedocs.io/', }, license='MIT', - python_requires='>=3.8', + python_requires='>=3.11', install_requires=[ - 'requests==2.25.1', + 'requests>=2.31', ], packages=find_packages(), include_package_data=False, diff --git a/tests/test_angular_separation.py b/tests/test_angular_separation.py index 71650e8..87c6eb6 100644 --- a/tests/test_angular_separation.py +++ b/tests/test_angular_separation.py @@ -1,8 +1,11 @@ """Test WGC angular separation calculation.""" -from pytest import fixture +from pytest import fixture, raises +from pytest import mark from webgeocalc import AngularSeparation +from webgeocalc.direction import Direction +from webgeocalc.errors import CalculationInvalidAttr, CalculationRequiredAttr @fixture @@ -45,8 +48,8 @@ def corr(): @fixture -def params(kernel_paths, time, target_1, target_2, observer, corr): - """Input parameters from WGC API example.""" +def params_two_targets(kernel_paths, time, target_1, target_2, observer, corr): + """Input parameters from WGC API example (TWO_TARGETS mode).""" return { 'kernel_paths': kernel_paths, 'times': time, @@ -58,8 +61,8 @@ def params(kernel_paths, time, target_1, target_2, observer, corr): @fixture -def payload(kernel_paths, time, target_1, target_2, observer, corr): - """Payload from WGC API example.""" +def payload_two_targets(kernel_paths, time, target_1, target_2, observer, corr): + """Payload from WGC API example (TWO_TARGETS mode).""" return { "kernels": [{ "type": "KERNEL", @@ -70,9 +73,7 @@ def payload(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", @@ -83,6 +84,121 @@ def payload(kernel_paths, time, target_1, target_2, observer, corr): } -def test_angular_separation_payload(params, payload): +@fixture +def kernel_set(): + """Input kernel set.""" + return 5 + + +@fixture +def direction_position(): + """Input position direction as dict object.""" + return { + "direction_type": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + } + + +@fixture +def direction_position_payload(): + """Position direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "POSITION", + "target": "SUN", + "shape": "POINT", + "observer": "CASSINI" + } + + +@fixture +def direction_vector(): + """Input vector direction as Direction object.""" + return Direction( + direction_type='VECTOR', + direction_vector_type='REFERENCE_FRAME_AXIS', + direction_frame='CASSINI_RPWS_EDIPOLE', + direction_frame_axis='Z', + ) + + +@fixture +def direction_vector_payload(): + """Vector direction payload.""" + return { + 'aberrationCorrection': 'NONE', + 'antiVectorFlag': False, + "directionType": "VECTOR", + "directionVectorType": "REFERENCE_FRAME_AXIS", + "directionFrame": "CASSINI_RPWS_EDIPOLE", + "directionFrameAxis": "Z" + } + + +@fixture +def params_two_directions( + kernel_set, time, direction_vector, + direction_position, +): + """Input parameters for TWO_DIRECTIONS mode.""" + return { + 'spec_type': 'TWO_DIRECTIONS', + 'kernels': kernel_set, + 'times': time, + 'direction_1': direction_vector, + 'direction_2': direction_position, + } + + +@fixture +def payload_two_directions( + kernel_set, time, direction_vector_payload, + direction_position_payload, +): + """Input parameters for TWO_DIRECTIONS mode.""" + return { + "kernels": [{ + "type": "KERNEL_SET", + "id": kernel_set, + }], + "specType": "TWO_DIRECTIONS", + "timeSystem": "UTC", + "timeFormat": "CALENDAR", + "times": [time], + "calculationType": "ANGULAR_SEPARATION", + "direction1": direction_vector_payload, + "direction2": direction_position_payload, + } + + +@mark.parametrize('spec_type', [ + ('TWO_TARGETS'), + ('TWO_DIRECTIONS'), +]) +def test_angular_separation_payload(request, spec_type): """Test angular separation payload.""" + params = request.getfixturevalue(f'params_{spec_type.lower()}') + payload = request.getfixturevalue(f'payload_{spec_type.lower()}') + assert AngularSeparation(**params).payload == payload + + +def test_angular_separation_errors(params_two_targets, params_two_directions, + kernel_set, time): + """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_cli.py b/tests/test_cli.py index d433fee..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,6 +197,111 @@ 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.""" + 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.""" + 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): """Test attribute in angular separation calculation parameter with the CLI.""" argv = '--kernels 1 --times 2012-10-19T08:24:00 --wrong 123'.split() @@ -365,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/tests/test_decorator.py b/tests/test_decorator.py index 6854597..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 a = A() diff --git a/tests/test_direction.py b/tests/test_direction.py new file mode 100644 index 0000000..7ec80d7 --- /dev/null +++ b/tests/test_direction.py @@ -0,0 +1,580 @@ +"""Test WGC direction setup.""" + +import re + +from pytest import raises + +from webgeocalc.direction import Direction +from webgeocalc.errors import (CalculationIncompatibleAttr, CalculationInvalidAttr, + CalculationRequiredAttr, CalculationUndefinedAttr) + + +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, + } + + +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', # only for direction VECTOR + ) + + # 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, + } + + +def test_direction_velocity_errors(): + """Test direction velocity 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_instrument_boresight(): + """Test direction vector with 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, + } + + # 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, + } + + +def test_direction_vector_frame_axis(): + """Test direction vector with 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, + } + + +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_ISS_NAC', + direction_vector_x=0, + direction_vector_y=0, + direction_vector_z=1, + ).payload == { + 'directionType': 'VECTOR', + 'directionVectorType': 'VECTOR_IN_INSTRUMENT_FOV', + '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_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', + ) + + # 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', + ) + + # 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 + ) + + +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', + ) + + # 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', + ) + + +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', + ) + + # 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', + ) + + # 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', + ) + + # 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/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/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 7a27075..99cadbb 100644 --- a/webgeocalc/calculation.py +++ b/webgeocalc/calculation.py @@ -4,11 +4,13 @@ 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, CalculationNotCompleted, CalculationRequiredAttr, CalculationTimeOut, CalculationUndefinedAttr) +from .payload import Payload from .types import KernelSetDetails from .vars import CALCULATION_FAILED_PHASES, VALID_PARAMETERS @@ -20,7 +22,7 @@ } -class Calculation: +class Calculation(Payload): """Webgeocalc calculation object. Parameters @@ -94,6 +96,12 @@ class Calculation: 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 @@ -151,16 +159,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 +187,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 +200,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 +441,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 @@ -1056,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. @@ -1456,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( @@ -1939,6 +1965,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 +2040,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) diff --git a/webgeocalc/calculation_types.py b/webgeocalc/calculation_types.py index bbc8b97..894a6ca 100644 --- a/webgeocalc/calculation_types.py +++ b/webgeocalc/calculation_types.py @@ -67,18 +67,12 @@ 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 - See: :py:attr:`shape_1` - shape_2: str, optional - See: :py:attr:`shape_2` - aberration_correction: str, optional - See: :py:attr:`aberration_correction` - - Other Parameters - ---------------- kernels: str, int, [str or/and int] See: :py:attr:`kernels` kernel_paths: str, [str] @@ -95,30 +89,70 @@ class AngularSeparation(Calculation): See: :py:attr:`time_system` time_format: str See: :py:attr:`time_format` + + 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` (default: ``POINT``) target_2: str or int See: :py:attr:`target_2` + shape_2: str, optional + See: :py:attr:`shape_2` (default: ``POINT``) observer: str or int See: :py:attr:`observer` + aberration_correction: str, optional + See: :py:attr:`aberration_correction` + + Required parameters `TWO_DIRECTIONS` + + direction_1: dict or Direction + See: :py:attr:`direction_1` + direction_2: dict or Direction + 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` + 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. """ - 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 - kwargs['aberration_correction'] = aberration_correction + + match spec_type: + case 'TWO_TARGETS': + self.REQUIRED += ('target_1', 'target_2', 'observer') + + for key in ['shape_1', 'shape_2']: + kwargs.setdefault(key, 'POINT') + + kwargs['aberration_correction'] = aberration_correction + + case 'TWO_DIRECTIONS': + self.REQUIRED += ('direction_1', 'direction_2') + + kwargs['spec_type'] = spec_type + + case _: + raise CalculationInvalidAttr( + 'spec_type', spec_type, VALID_PARAMETERS['SPEC_TYPE'], + ) super().__init__(**kwargs) @@ -174,7 +208,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). diff --git a/webgeocalc/cli.py b/webgeocalc/cli.py index 3aec341..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 @@ -289,47 +306,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..285a1dc --- /dev/null +++ b/webgeocalc/direction.py @@ -0,0 +1,671 @@ +"""Webgeocalc Directions.""" + +from .decorator import parameter +from .errors import (CalculationIncompatibleAttr, CalculationInvalidAttr, + CalculationUndefinedAttr) +from .payload import Payload +from .vars import VALID_PARAMETERS + + +class Direction(Payload): + """Webgeocalc direction object. + + 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` + Not required if :py:attr:`aberration_correction` is ``NONE`` + and :py:attr:`direction vector` is ``VECTOR``. + + Required parameters 'POSITION' + + target: str or int + See: :py:attr:`target` + shape: str + See: :py:attr:`shape` + + Required parameters 'VELOCITY' + + target: str or int + See: :py:attr:`target` + reference_frame: str, optional + See: :py:attr:`reference_frame` + + Required parameters 'VECTOR' + + 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` + + 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', ) + + 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(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 :py:class:`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') + + 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 direction. + + Required only if :py:attr:`direction_vector_type` 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 :py:attr:`direction_vector_type` 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 :py:attr:`direction_vector_type` 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 :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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + direction_vector_az: float + The azimuth direction vector coordinate value. + + """ + self._required('azccw_flag') + self.__directionVectorAz = self._vector('az', val) + + @parameter + def direction_vector_el(self, val): + """The elevation direction vector coordinate. + + 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. + + Parameters + ---------- + direction_vector_el: float + The elevation vector coordinate value. + + """ + self._required('elplsz_flag') + self.__directionVectorEl = self._vector('el', val) + + @parameter(only='BOOLEAN') + def azccw_flag(self, val): + """Flag indicating how azimuth is measured. + + 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. + + Parameters + ---------- + 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 :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. + + Parameters + ---------- + 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 + + @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/errors.py b/webgeocalc/errors.py index b7aeb56..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) @@ -132,5 +134,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..2dcf65f --- /dev/null +++ b/webgeocalc/payload.py @@ -0,0 +1,65 @@ +"""WebGeoCalc Payload submodule.""" + +from abc import ABC + +from .errors import CalculationRequiredAttr + + +class Payload(ABC): + """Abstract WebGeoCalc payload abstract 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. + + """ + return dict(self) diff --git a/webgeocalc/vars.py b/webgeocalc/vars.py index 677d646..b21adbf 100644 --- a/webgeocalc/vars.py +++ b/webgeocalc/vars.py @@ -97,6 +97,19 @@ 'CN', 'CN+S', ], + 'ABERRATION_CORRECTION_VECTOR': [ + 'NONE', + 'LT', + 'CN', + 'XLT', + 'XCN', + 'S', + 'XS', + ], + 'SPEC_TYPE': [ + 'TWO_TARGETS', + 'TWO_DIRECTIONS', + ], 'STATE_REPRESENTATION': [ 'RECTANGULAR', 'RA_DEC', @@ -112,6 +125,10 @@ 'ELLIPSOID', 'DSK ', ], + 'TARGET_SHAPE': [ + 'POINT', + 'SPHERE', + ], 'TIME_LOCATION': [ 'FRAME1', 'FRAME2', @@ -158,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', @@ -206,5 +216,27 @@ 'ABSMIN', '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', + ], }