ptwrapper 2.7.6__tar.gz → 2.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/PKG-INFO +1 -1
  2. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/pyproject.toml +1 -1
  3. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/subscribers.py +82 -30
  4. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/utils.py +45 -0
  5. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/LICENSE.txt +0 -0
  6. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/README.md +0 -0
  7. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/.flake8 +0 -0
  8. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/__init__.py +0 -0
  9. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/cli.py +0 -0
  10. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/agm/cfg_agm_jui.xml +0 -0
  11. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/agm/cfg_agm_jui_event_definitions.xml +0 -0
  12. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/agm/cfg_agm_jui_fixed_definitions.xml +0 -0
  13. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/agm/cfg_agm_jui_predefined_block.xml +0 -0
  14. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/BRF_MAL_SGICD_2_1_300101_351005.brf +0 -0
  15. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/RES_C50_SA_CELLS_EFFICIENCY_310101_351003.csv +0 -0
  16. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/eps.cfg +0 -0
  17. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/events.juice.def +0 -0
  18. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/phs_com_res_sa_cells_count.asc +0 -0
  19. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/eps/units.def +0 -0
  20. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/config/session_file.json +0 -0
  21. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/html_log.py +0 -0
  22. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/TOP__events.evf +0 -0
  23. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/TOP_crema_5_0_events.evf +0 -0
  24. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/TOP_crema_5_1_150lb_23_1_a3_events.evf +0 -0
  25. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/TOP_crema_5_1_150lb_23_1_events.evf +0 -0
  26. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/downlink.evf +0 -0
  27. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/EDF_JUI_SPC_LINK_KAB.edf +0 -0
  28. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/EDF_JUI_SPC_LINK_XB.edf +0 -0
  29. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/TBD.edf +0 -0
  30. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/TOP_experiments.edf +0 -0
  31. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/juice__spacecraft.edf +0 -0
  32. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/juice__spacecraft_platform.edf +0 -0
  33. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/edf/juice__spacecraft_ssmm.edf +0 -0
  34. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/evf/EVT_CREMA_5_0_GEOPIPELINE.EVF +0 -0
  35. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/evf/EVT_CREMA_5_1_150LB_23_1_A3_GEOPIPELINE.EVF +0 -0
  36. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/evf/EVT_CREMA_5_1_150LB_23_1_GEOPIPELINE.EVF +0 -0
  37. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/evf/EVT__GEOPIPELINE.EVF +0 -0
  38. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/itl/TBD.itl +0 -0
  39. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/itl/TOP_timelines.itl +0 -0
  40. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/itl/downlink.itl +0 -0
  41. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/input/itl/platform.itl +0 -0
  42. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/main.py +0 -0
  43. {ptwrapper-2.7.6 → ptwrapper-2.7.7}/src/ptwrapper/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ptwrapper
3
- Version: 2.7.6
3
+ Version: 2.7.7
4
4
  Summary: A Pointing Tool OSVE wrapper
5
5
  License: European Space Agency Public License (ESA-PL) Permissive (Type 3) – v2.4
6
6
  License-File: LICENSE.txt
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ptwrapper"
3
- version = "2.7.6"
3
+ version = "2.7.7"
4
4
  description = "A Pointing Tool OSVE wrapper"
5
5
  authors = [
6
6
  "Marc Costa <marc.costa@ext.esa.int>",
@@ -8,14 +8,17 @@
8
8
  # #
9
9
  # (C) Copyright European Space Agency, 2025 #
10
10
  # *************************************************************************** #
11
+ from asyncio import events
11
12
  import os
12
13
 
13
14
  import numpy as np
14
15
  import spiceypy as spice
15
16
  from osve.subscribers.osve_ptr_abstract import OsvePtrAbstract
16
- from .utils import log
17
+ from .utils import get_evf_path, log, parse_events_from_evf
17
18
  import xml.etree.ElementTree as ET
18
19
  from spiceypy.utils.exceptions import SpiceyError
20
+ from datetime import datetime
21
+ import pprint
19
22
 
20
23
 
21
24
  class OsvePtrLogger(OsvePtrAbstract):
@@ -32,6 +35,10 @@ class OsvePtrLogger(OsvePtrAbstract):
32
35
 
33
36
  remove_checks = True
34
37
 
38
+ # Retrieve eclipse events (geopipeline file) to mask panel illuminations
39
+ evf_file = get_evf_path()
40
+ eclipse_events = parse_events_from_evf(evf_file, keyword="SUN_OCC")
41
+
35
42
  # Initialize attributes
36
43
  swi_observations = []
37
44
  swi_pointing_check = False
@@ -66,7 +73,7 @@ class OsvePtrLogger(OsvePtrAbstract):
66
73
  px_illumination_block_violation = False
67
74
  pz_illumination_block_violation = False
68
75
  janus_sun_exclusion_block_violation = False
69
-
76
+ block_mode = None
70
77
 
71
78
  def __init__(self, meta_kernel, remove_obs_comp=False, remove_checks=True):
72
79
  """
@@ -102,6 +109,7 @@ class OsvePtrLogger(OsvePtrAbstract):
102
109
  if rules_xml:
103
110
  obs.update(self._parse_numeric_subnodes(rules_xml))
104
111
 
112
+ self._get_pointing_block_mode(block_data)
105
113
  self._handle_swi_checks(block_data)
106
114
  self._handle_pephi_jeni_checks(block_data)
107
115
  self._handle_peplo_checks(block_data)
@@ -109,6 +117,10 @@ class OsvePtrLogger(OsvePtrAbstract):
109
117
  self._handle_pz_illumination_check(block_data)
110
118
  self._handle_janus_sun_exclusion_check(block_data)
111
119
  return 0
120
+
121
+ def _get_pointing_block_mode(self, block_data):
122
+ if block_data["block_type"] != "SLEW":
123
+ self.block_mode = block_data["block_mode"]
112
124
 
113
125
  def _handle_swi_checks(self, block_data):
114
126
  if self.remove_obs_comp or "observations" not in block_data:
@@ -123,7 +135,7 @@ class OsvePtrLogger(OsvePtrAbstract):
123
135
  obs["swi_drift_check"] = True
124
136
  obs["swi_point_violation"] = False
125
137
  obs["swi_drift_violation"] = False
126
- obs["swi_et_prev"] = 0.0
138
+ obs["swi_et_prev"] = None
127
139
  obs["swi_x_t_prev"] = None
128
140
  obs["swi_y_t_prev"] = None
129
141
  self.swi_observations.append(obs)
@@ -241,16 +253,26 @@ class OsvePtrLogger(OsvePtrAbstract):
241
253
  self._set_pephi_jeni_target(obs, block_data)
242
254
 
243
255
  def _set_pephi_jeni_target(self, observation, block_data):
244
- try:
245
- target = observation.get("target", "JUPITER")
246
- if '.' in target:
247
- target = target.split('.')[-1]
248
- self.pephi_jeni_target = target
249
- except Exception:
250
- self.pephi_jeni_target = 'JUPITER'
256
+ target = observation.get("target")
257
+ if target is not None and '.' in target:
258
+ target = target.split('.')[-1].upper()
259
+ # Check SPICE target ID
260
+ try:
261
+ code = spice.bodn2c(target)
262
+ if code < 0: code = None # Not a celestial body
263
+ except SpiceyError:
264
+ code = None
265
+ else:
266
+ code = None
267
+
268
+ # If target is not valid -> set JUPITER as default value
269
+ if code is None:
270
+ target = 'JUPITER'
251
271
  warning_msg = f'PEPHI JENI observation {observation.get("obsId", "UNKNOWN")} has no target, setting to JUPITER as default.'
252
- log('WARNING', 'PTWR', block_data["block_start"], warning_msg)
253
- self.onMsgReceived('WARNING', 'PTWR', block_data["block_start"], warning_msg)
272
+ log('INFO', 'PTWR', block_data["block_start"], warning_msg)
273
+ self.onMsgReceived('INFO', 'PTWR', block_data["block_start"], warning_msg)
274
+
275
+ self.pephi_jeni_target = target
254
276
 
255
277
  def _handle_peplo_checks(self, block_data):
256
278
  if self.remove_obs_comp or "observations" not in block_data:
@@ -261,8 +283,19 @@ class OsvePtrLogger(OsvePtrAbstract):
261
283
  # Future implementation
262
284
  # self.peplo_nim_check = True
263
285
  pass
286
+
287
+ def _is_in_eclipse(self, time_str):
288
+ current_time = datetime.strptime(time_str.replace("Z", ""), "%Y-%m-%dT%H:%M:%S")
289
+ for e in self.eclipse_events:
290
+ if e["startTime"] <= current_time <= e["endTime"]:
291
+ return True
292
+ return False
264
293
 
265
294
  def _handle_px_illumination_check(self, block_data):
295
+ #self.px_illumination_block_violation =
296
+ # We implement here masking for eclipse periods if the +X panel illumination is during an eclipse,
297
+ # therefore, even if AGM report a +X panel illumination, we do not report a violation at the start
298
+ # of the block if the +X panel illumination is masked by an eclipse event in the EVF file (geopipeline)
266
299
  self.px_illumination_block_violation = bool(block_data.get('px_illumination'))
267
300
 
268
301
  def _handle_pz_illumination_check(self, block_data):
@@ -329,6 +362,8 @@ class OsvePtrLogger(OsvePtrAbstract):
329
362
  log('ERROR', 'PTWR', blockdata["block_end"], 'Block has +X Panel Illuminated')
330
363
  self.onMsgReceived('ERROR', 'PTWR', blockdata["block_end"],
331
364
  'Block has +X Panel Illuminated')
365
+
366
+ self.px_illumination_block_violation = False # restart
332
367
 
333
368
  # --------------------------------------------------------------------------------------------------------------
334
369
  # +Z Illumination Constraint Check per pointing block
@@ -459,7 +494,7 @@ class OsvePtrLogger(OsvePtrAbstract):
459
494
  # ----------------------------------------------------------------------------------------------------------
460
495
  # +X Illumination Constraint Check per pointing block - Complements AGM constraint.
461
496
  # ----------------------------------------------------------------------------------------------------------
462
- if data['px_illumination']:
497
+ if data['px_illumination'] and not self._is_in_eclipse(data["time"]):
463
498
  self.px_illumination_block_violation = True
464
499
 
465
500
  # ----------------------------------------------------------------------------------------------------------
@@ -501,7 +536,7 @@ class OsvePtrLogger(OsvePtrAbstract):
501
536
  if key in ['ATDRIFT', 'CTDRIFT'] and not reportDrift:
502
537
  continue
503
538
 
504
- msg = f'[{type}] [{obsId}] {value}'
539
+ msg = f'[{type}] [{obsId}] [MODE: {self.block_mode}] {value}'
505
540
  log(severity, 'PTWR', f'{time}Z', msg)
506
541
  self.onMsgReceived(severity, 'PTWR', f'{time}Z', msg)
507
542
 
@@ -589,7 +624,6 @@ class OsvePtrLogger(OsvePtrAbstract):
589
624
  else:
590
625
  severity = ["ERROR", "WARNING"]
591
626
 
592
- # Iterate over each log and check if it matches the condition
593
627
  for log in logs:
594
628
  severity_matches = log["severity"] in severity
595
629
  module_matches = log["module"] in ["AGM", "AGE", "PTWR"]
@@ -607,10 +641,14 @@ class OsvePtrLogger(OsvePtrAbstract):
607
641
  Processes a standard (non-SLEW) block and updates the log.
608
642
  """
609
643
  designer, designer_obs = self._get_designer_and_obs(block_data)
610
- if verbose:
611
- self._print_block_summary(idx, designer, designer_obs, block_data["block_start"], block_data["block_end"], block_data["status"])
612
644
 
613
645
  error_messages = self._extract_error_messages(block_data, verbose)
646
+ # If error_messages is empty, it means there are no relevant logs to report for this block, so we can skip logging it.
647
+ # This is particularly relevant for the case of error blocks that have been masked here (e.g., panel illumination during eclipse events)
648
+ if not error_messages: return
649
+
650
+ if verbose:
651
+ self._print_block_summary(idx, designer, designer_obs, block_data["block_start"], block_data["block_end"], block_data["status"])
614
652
 
615
653
  try:
616
654
  block_status = str(block_data["status"])
@@ -656,12 +694,15 @@ class OsvePtrLogger(OsvePtrAbstract):
656
694
  if prev_info is None or next_info is None:
657
695
  log('WARNING', 'PTWR', '', f'The SLEW block {idx - 1} cannot be logged')
658
696
  return
697
+
698
+ # If error_messages is empty, it means there are no relevant logs to report for this block, so we can skip logging it.
699
+ # This is particularly relevant for the case of error blocks that have been masked here (e.g., panel illumination during eclipse events)
700
+ error_messages = self._extract_error_messages(block_data, verbose, slew_prev=prev_info, slew_next=next_info)
701
+ if not error_messages: return
659
702
 
660
703
  if verbose:
661
704
  self._print_slew_summary(idx, prev_info, next_info)
662
705
 
663
- error_messages = self._extract_error_messages(block_data, verbose, slew_prev=prev_info, slew_next=next_info)
664
-
665
706
  try:
666
707
  self._update_slew_log(ptr_log, prev_info, next_info, idx, error_messages)
667
708
  except (ValueError, KeyError, TypeError) as e:
@@ -755,12 +796,16 @@ class OsvePtrLogger(OsvePtrAbstract):
755
796
  exec_percentage = self._calculate_execution_percentage(time_exec, time_start, time_end)
756
797
 
757
798
  ##Temporary fix
799
+ # 1. Increase slew violation severity if "above maximum allowed" is found in the log text, as this is a common message for slew violations in AGM.
758
800
  if "above maximum allowed" in log['text']:
759
801
  log['severity'] = 'ERROR'
760
-
802
+ # 2. Mask +X panel illumination messages during eclipse periods
803
+ if ("Panel Illumination" in log.get("text", "") and self._is_in_eclipse(log.get("time", ""))):
804
+ continue
805
+
761
806
  if verbose:
762
807
  print(f" {log['severity']} , {exec_percentage}, {log['time']} , {log['text']}")
763
-
808
+
764
809
  error_messages.append(self._format_error(log, exec_percentage))
765
810
 
766
811
  return error_messages
@@ -947,25 +992,31 @@ class OsvePtrLogger(OsvePtrAbstract):
947
992
  y_t_prev = observation.get("swi_y_t_prev")
948
993
 
949
994
  # The constraint is only checked every 30 seconds.
950
- if et_prev:
995
+ if et_prev is not None:
951
996
  dt = et - et_prev
952
997
  if dt < 30:
953
998
  return None, None, None, None
954
999
 
955
- # 1) Spacecraft's known quaternion (J2000 -> s/c)
1000
+ # 2) Spacecraft's known quaternion (J2000 -> s/c)
956
1001
  q_t = np.array([data['qs'], data['q1'], data['q2'], data['q3']])
957
1002
  q_t = q_t / np.linalg.norm(q_t) # make sure it's unit
958
1003
 
959
- # 2) Position of the target w.r.t. s/c in J2000 at time et
1004
+ # 3) Position of the target w.r.t. s/c in J2000 at time et
960
1005
  # (state_t = [x, y, z, vx, vy, vz], ignoring velocity index here)
961
- target, _ = self._get_swi_target(observation)
962
- state_t, _ = spice.spkezr(target, et, "JUICE_SPACECRAFT", "LT+S", "JUICE")
963
- x_t, y_t = self._swi_get_xy_from_body_vector(state_t)
1006
+ target, _ = self._get_swi_target(observation)
1007
+ state_t, _ = spice.spkezr(target, et, "J2000", "LT+S", "JUICE")
1008
+ r_j2000_t = state_t[:3]
1009
+
1010
+ # 4) Rotation matrix from J2000 to s/c
1011
+ r_j2000_to_sc = spice.q2m(q_t)
964
1012
 
965
- # 3) Obtain (x, y) drift rates in deg/s
966
- if et_prev != 0.:
1013
+ # 5) Transform target vector into s/c frame, compute (x, y) at time t
1014
+ rb_t = spice.mxv(r_j2000_to_sc, r_j2000_t)
1015
+ x_t, y_t = self._swi_get_xy_from_body_vector(rb_t)
1016
+
1017
+ if et_prev is not None:
967
1018
  dt = et - et_prev
968
- # 12) Approximate drift rate in deg/s
1019
+ # 6) Approximate drift rate in deg/s
969
1020
  dx_dt = (x_t - x_t_prev) / dt
970
1021
  dy_dt = (y_t - y_t_prev) / dt
971
1022
  else:
@@ -978,6 +1029,7 @@ class OsvePtrLogger(OsvePtrAbstract):
978
1029
 
979
1030
  return x_t, y_t, dx_dt, dy_dt
980
1031
 
1032
+
981
1033
  def _swi_block_duration_check(self, blockdata, observation_definition):
982
1034
  """
983
1035
  Check if the SWI observation block's duration meets the minimum required duration.
@@ -12,6 +12,8 @@ import json
12
12
  import os
13
13
  import shutil
14
14
  import re
15
+ from datetime import datetime
16
+ from pathlib import Path
15
17
 
16
18
 
17
19
  class MyError(Exception):
@@ -317,3 +319,46 @@ def crema_identifier(metakernel_path):
317
319
  def log(level, tag, time, message):
318
320
  severity = f"[{level}]"
319
321
  print(f"{severity:<10}<{tag:<4}> {time:<20} {message}")
322
+
323
+ def get_evf_path():
324
+ base_dir = Path(__file__).resolve().parent
325
+ evf_path = base_dir / "input" / "evf" / "EVT_CREMA_5_1_150LB_23_1_A3_GEOPIPELINE.EVF"
326
+ return evf_path
327
+
328
+ def parse_events_from_evf(evf_file_path, keyword=None):
329
+ open_events = {}
330
+ parsed_events = []
331
+ with open(evf_file_path, 'r') as file:
332
+ for line in file:
333
+ line = line.strip()
334
+ if not line or line.startswith('#'): # Skip empty lines and comments
335
+ continue
336
+
337
+ # Get parts of the line, expecting at least a time and an event name
338
+ parts = line.split()
339
+ if len(parts) < 2:
340
+ continue # Skip lines that don't have at least a time and an event name
341
+ time_str = parts[0]
342
+ event_name = parts[1]
343
+
344
+ # Convert time
345
+ event_time = datetime.strptime(time_str, "%d-%b-%Y_%H:%M:%S")
346
+
347
+ # If a keyword is provided, filter events by the keyword in the event name
348
+ if keyword and keyword.upper() not in event_name.upper():
349
+ continue
350
+
351
+ # Handle start/end pairing
352
+ if event_name.endswith("_START"):
353
+ base_name = event_name[:-6]
354
+ open_events[base_name] = event_time
355
+ elif event_name.endswith("_END"):
356
+ base_name = event_name[:-4]
357
+ if base_name in open_events:
358
+ parsed_events.append({
359
+ "name": base_name,
360
+ "startTime": open_events.pop(base_name),
361
+ "endTime": event_time
362
+ })
363
+
364
+ return parsed_events
File without changes
File without changes