cgse-common 2023.1.4__py3-none-any.whl → 2024.1.4__py3-none-any.whl

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.
egse/hk.py ADDED
@@ -0,0 +1,794 @@
1
+ __all__ = [
2
+ "get_housekeeping",
3
+ "convert_hk_names",
4
+ "read_conversion_dict",
5
+ "TmDictionaryColumns",
6
+ ]
7
+ import csv
8
+ import datetime
9
+ import logging
10
+ from enum import Enum
11
+ from pathlib import Path
12
+ from typing import Optional
13
+ from typing import Union
14
+
15
+ import dateutil.parser as date_parser
16
+ import numpy as np
17
+ from egse.config import find_files
18
+ from egse.env import get_data_storage_location
19
+ from egse.obsid import ObservationIdentifier
20
+ from egse.obsid import obsid_from_storage
21
+ from egse.settings import Settings
22
+ from egse.setup import Setup
23
+ from egse.setup import SetupError
24
+ from egse.setup import load_setup
25
+ from egse.system import SECONDS_IN_A_DAY
26
+ from egse.system import read_last_line
27
+ from egse.system import read_last_lines
28
+ from egse.system import str_to_datetime
29
+ from egse.system import time_since_epoch_1958
30
+
31
+ _LOGGER = logging.getLogger(__name__)
32
+
33
+ SITE_ID = Settings.load("SITE").ID
34
+
35
+
36
+ class TmDictionaryColumns(str, Enum):
37
+ """ Enumeration of the relevant columns in the TM dictionary spreadsheet.
38
+
39
+ The relevant columns are:
40
+
41
+ - STORAGE_MNEMONIC: Column with the storage mnemonic of the process that generated the HK;
42
+ - CORRECT_HK_NAMES: Column with the correct HK names (that can be used in `get_housekeeping`);
43
+ - ORIGINAL_EGSE_HK_NAMES: Column with the names that were originally used in `get_housekeeping` the device
44
+ protocol;
45
+ - SYNOPTICS_ORIGIN: Column with the origin of the synoptics at the current site;
46
+ - TIMESTAMP_NAMES: Column with the name of the timestamps.
47
+ - DASHBOARD: Column with the name of the dashboard that holds the HK metric
48
+ """
49
+
50
+ STORAGE_MNEMONIC = "Storage mnemonic"
51
+ CORRECT_HK_NAMES = "CAM EGSE mnemonic"
52
+ ORIGINAL_EGSE_HK_NAMES = "Original name in EGSE"
53
+ SYNOPTICS_ORIGIN = f"Origin of synoptics at {SITE_ID}"
54
+ TIMESTAMP_NAMES = "Name of corresponding timestamp"
55
+ DESCRIPTION = "Description"
56
+ DASHBOARD = "MON screen"
57
+
58
+
59
+ class HKError(Exception):
60
+ """An HK-specific error."""
61
+ pass
62
+
63
+
64
+ def get_housekeeping(hk_name: str, obsid: Union[ObservationIdentifier, str, int] = None, od: str = None,
65
+ time_window: int = None, data_dir=None, setup: Optional[Setup] = None):
66
+ """
67
+ Returns the timestamp(s) and housekeeping value(s) for the housekeeping parameter with the given name.
68
+
69
+ It is possible to indicate for which obsid or which OD the housekeeping is to be returned. If neither of them is
70
+ specified, the latest daily files are used.
71
+
72
+ When the time window has not been specified, the last timestamp and housekeeping value will be returned for the
73
+ given OD. It is possible that a component stopped writing HK for some reason, and that the last housekeeping value
74
+ is older than you would want. It is therefore important to inspect the corresponding timestamp.
75
+
76
+ When the time window has been specified, the relevant housekeeping will be read:
77
+
78
+ * determine the sampling rate (compare the timestamps for the last 2 lines in the housekeeping file);
79
+ * determine how many samples we need to read (starting at the back);
80
+ * read the required number of line, starting at the back;
81
+ * for each of the read lines, append the timestamp and HK value to the arrays that will be returned
82
+
83
+ Args:
84
+ hk_name: Name of the housekeeping parameter.
85
+ obsid: Observation identifier. This can be an ObservationIdentifier object, a string in format TEST_LAB or
86
+ TEST_LAB_SETUP, or an integer representing the test ID; optional.
87
+ od: Identifier for the OD (yyyymmdd); optional.
88
+ time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at
89
+ the moment this method is called. If not given, the latest housekeeping value is returned.
90
+ data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored. If this argument is not
91
+ provided, the data_dir will be determined from the environment variable PLATO_DATA_STORAGE_LOCATION.
92
+ setup: Setup
93
+
94
+ Raises:
95
+ A HKError when one of the following problems occur
96
+ * no obsid nor an od argument was provided
97
+ * no HK measures were found for the given parameter and obsid/od
98
+
99
+ Returns:
100
+ - If the time window has not been specified: the most recent timestamp and housekeeping value.
101
+ - If the time window has been specified: an array of timestamps and an array of housekeeping values, belonging
102
+ to the specified time window.
103
+
104
+ """
105
+
106
+ setup = setup or load_setup()
107
+
108
+ # Either specify the obsid or the OD (or neither of them) but not both
109
+
110
+ if obsid is not None and od is not None:
111
+
112
+ raise HKError(f"Both the obsid ({obsid}) and the OD ({od}) were specified.")
113
+
114
+ # Specified obsid (as integer or as string)
115
+
116
+ data_dir = data_dir or get_data_storage_location()
117
+
118
+ if obsid:
119
+
120
+ try:
121
+ return _get_housekeeping_obsid(hk_name, data_dir, obsid=obsid, time_window=time_window, setup=setup)
122
+ except (ValueError, StopIteration, FileNotFoundError) as exc:
123
+ raise HKError(f"No HK found for {hk_name} for obsid {obsid} at {SITE_ID}") from exc
124
+
125
+ # Specified OD
126
+
127
+ if od:
128
+
129
+ try:
130
+ return _get_housekeeping_od(hk_name, data_dir, od=od, time_window=time_window, setup=setup)
131
+ except (ValueError, StopIteration, FileNotFoundError) as exc:
132
+ raise HKError(f"No HK found for {hk_name} for OD {od} at {SITE_ID}") from exc
133
+
134
+ # Didn't specify neither the obsid nor the OD
135
+
136
+ try:
137
+ return _get_housekeeping_daily(hk_name, data_dir, time_window=time_window, setup=setup)
138
+ except (ValueError, StopIteration, FileNotFoundError) as exc:
139
+ raise HKError(f"No HK found for {hk_name} for today at {SITE_ID}") from exc
140
+
141
+
142
+ def _get_housekeeping(hk_name: str, timestamp_name: str, hk_dir: str, files, time_window: int = None):
143
+ """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name, for the given files.
144
+
145
+ When the time window has not been specified, the last timestamp and HK value will be returned for the given OD.
146
+ It is possible that a component stopped writing HK for some reason, and that the last HK value is older than you
147
+ would want. It is therefore important to inspect the corresponding timestamp.
148
+
149
+ When the time window has been specified, the relevant HK will be read:
150
+ - determine the sampling rate (compare the timestamps for the last 2 lines in the HK file);
151
+ - determine how many samples we need to read (starting at the back);
152
+ - read the required number of line, starting at the back;
153
+ - for each of the read lines, append the timestamp and HK value to the arrays that will be returned
154
+
155
+ Args:
156
+ - hk_name: Name of the housekeeping parameter.
157
+ - timestamp_name: Name of the corresponding timestamp.
158
+ - hk_dir: Directory with the housekeeping files.
159
+ - files: Relative filepath of the selected housekeeping files.
160
+ - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at
161
+ the moment this method is called. If not given, the latest HK-value is returned.
162
+
163
+ Returns:
164
+ - If the time window has not been specified: the most recent timestamp and housekeeping value.
165
+ - If the time window has been specified: an array of timestamps and an array of housekeeping values, belonging
166
+ to the specified time window.
167
+ """
168
+
169
+ filename = files[-1]
170
+
171
+ # Indices of the columns we need
172
+
173
+ timestamp_index, hk_index = get_indices(hk_dir + filename, hk_name, timestamp_name)
174
+
175
+ # No time window specified: return the last value
176
+
177
+ if time_window is None:
178
+
179
+ return get_last_non_empty(hk_dir + filename, timestamp_index, hk_index)
180
+
181
+ # Time window specified
182
+
183
+ else:
184
+
185
+ # We will return an array of timestamps and an array of HK values
186
+
187
+ timestamp_array = np.array([])
188
+ hk_array = np.array([])
189
+
190
+ with open(hk_dir + filename) as file:
191
+
192
+ csv_reader = csv.reader(file)
193
+ next(csv_reader)
194
+ first_timepoint = next(csv_reader)[0].split(",")[timestamp_index] # Skip the header
195
+
196
+ last_timepoint = read_last_line(hk_dir + filename).split(",")[timestamp_index]
197
+ elapsed = (str_to_datetime(last_timepoint) - str_to_datetime(first_timepoint)).total_seconds()
198
+
199
+ # The time window is shorter than the timespan covered by the file
200
+
201
+ if time_window < elapsed:
202
+
203
+ sampling_rate = get_sampling_rate(hk_dir + filename, timestamp_name) # Time between subsequent samples
204
+ num_samples = int(round(time_window / sampling_rate))
205
+
206
+ lines = read_last_lines(hk_dir + filename, num_samples)
207
+
208
+ for line in lines:
209
+
210
+ line = line.split(",")
211
+
212
+ timestamp_array = np.append(timestamp_array, line[timestamp_index])
213
+ hk_array = np.append(hk_array, line[hk_index])
214
+
215
+ # The time window is longer than the timespan covered by the file: read all lines
216
+
217
+ else:
218
+
219
+ with open(hk_dir + filename) as file:
220
+
221
+ csv_reader = csv.reader(file)
222
+ next(csv_reader) # Skip the header
223
+
224
+ for row in csv_reader:
225
+
226
+ timestamp_array = np.append(timestamp_array, row[timestamp_index])
227
+ hk_array = np.append(hk_array, row[hk_index])
228
+
229
+ for index in range(len(timestamp_array)):
230
+
231
+ timestamp_array[index] = time_since_epoch_1958(timestamp_array[index])
232
+
233
+ return timestamp_array, hk_array
234
+
235
+
236
+ def _get_housekeeping_od(hk_name: str, data_dir, od: str, time_window: int = None, setup: Optional[Setup] = None):
237
+ """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name, for the given OD.
238
+
239
+ When the time window has not been specified, the last timestamp and HK value will be returned for the given OD.
240
+ It is possible that a component stopped writing HK for some reason, and that the last HK value is older than you
241
+ would want. It is therefore important to inspect the corresponding timestamp.
242
+
243
+ When the time window has been specified, the relevant HK will be read:
244
+ - determine the sampling rate (compare the timestamps for the last 2 lines in the HK file);
245
+ - determine how many samples we need to read (starting at the back);
246
+ - read the required number of line, starting at the back;
247
+ - for each of the read lines, append the timestamp and HK value to the arrays that will be returned
248
+
249
+ Args:
250
+ - hk_name: Name of the housekeeping parameter.
251
+ - data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored.
252
+ - od: Identifier for the OD (yyyymmdd).
253
+ - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at
254
+ the moment this method is called. If not given, the latest HK-value is returned.
255
+ - setup: Setup.
256
+
257
+ Returns:
258
+ - If the time window has not been specified: the most recent timestamp and HK value.
259
+ - If the time window has been specified: an array of timestamps and an array of HK values, belonging to the
260
+ specified time window.
261
+ """
262
+
263
+ setup = setup or load_setup()
264
+ hk_dir = f"{data_dir}/daily/" # Where the HK is stored
265
+
266
+ try:
267
+
268
+ origin, timestamp_name = get_hk_info(hk_name, setup=setup)
269
+
270
+ except KeyError:
271
+
272
+ raise HKError(f"Cannot determine which EGSE component generated HK parameter {hk_name}")
273
+
274
+ hk_dir += f"{od}/"
275
+ hk_files = [f"{od}_{SITE_ID}_{origin}.csv"]
276
+
277
+ return _get_housekeeping(hk_name, timestamp_name, hk_dir, hk_files, time_window=time_window)
278
+
279
+
280
+ def _get_housekeeping_obsid(hk_name: str, data_dir, obsid: Union[ObservationIdentifier, str, int],
281
+ time_window: int = None, setup: Optional[Setup] = None):
282
+ """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name, for the given obsid.
283
+
284
+ When the time window has not been specified, the last timestamp and HK value will be returned for the given obsid.
285
+ It is possible that a component stopped writing HK for some reason, and that the last HK value is older than you
286
+ would want. It is therefore important to inspect the corresponding timestamp.
287
+
288
+ When the time window has been specified, the relevant HK will be read:
289
+ - determine the sampling rate (compare the timestamps for the last 2 lines in the HK file);
290
+ - determine how many samples we need to read (starting at the back);
291
+ - read the required number of line, starting at the back;
292
+ - for each of the read lines, append the timestamp and HK value to the arrays that will be returned
293
+
294
+ Args:
295
+ - hk_name: Name of the housekeeping parameter.
296
+ - data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored.
297
+ - obsid: Observation identifier. This can be an ObservationIdentifier object, a string in format TEST_LAB or
298
+ TEST_LAB_SETUP, or an integer representing the test ID.
299
+ - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at
300
+ the moment this method is called. If not given, the latest HK-value is returned.
301
+ - setup: Setup.
302
+
303
+ Returns:
304
+ - If the time window has not been specified: the most recent timestamp and HK value.
305
+ - If the time window has been specified: an array of timestamps and an array of HK values, belonging to the
306
+ specified time window.
307
+ """
308
+
309
+ setup = setup or load_setup()
310
+
311
+ hk_dir = f"{data_dir}/obs/" # Where the HK is stored
312
+
313
+ try:
314
+
315
+ origin, timestamp_name = get_hk_info(hk_name, setup=setup)
316
+
317
+ except KeyError:
318
+
319
+ raise HKError(f"Cannot determine which EGSE component generated HK parameter {hk_name}")
320
+
321
+ obsid = obsid_from_storage(obsid, data_dir=data_dir) # Convert the obsid to the correct format
322
+
323
+ hk_dir += f"{obsid}/"
324
+ pattern = f"{obsid}_{origin}_*.csv"
325
+ hk_files = sorted(find_files(pattern=pattern, root=hk_dir))
326
+
327
+ if len(hk_files) == 0:
328
+
329
+ raise HKError(f"No HK found for the {origin} at {SITE_ID} for obsid {obsid}")
330
+
331
+ hk_files = [hk_files[-1].name]
332
+
333
+ return _get_housekeeping(hk_name, timestamp_name, hk_dir, hk_files, time_window=time_window)
334
+
335
+
336
+ def _get_housekeeping_daily(hk_name: str, data_dir, time_window: int = None, setup: Optional[Setup] = None):
337
+ """ Return the timestamp(s) and HK value(s) for the HK parameter with the given name.
338
+
339
+ When the time window has not been specified, the last timestamp and HK value will be returned. It is possible that
340
+ a component stopped writing HK for some reason, and that the last HK value is older than you would want. It is
341
+ therefore important to inspect the corresponding timestamp.
342
+
343
+ When the time window has been specified, it is possible that we have to fetch the HK from multiple files,
344
+ depending on the length of the time window:
345
+
346
+ * Oldest file (i.e. the HK file in which the first timestamp in the specified time window is held): only part
347
+ of it will have to be read (start reading at the back):
348
+ - determine the filename;
349
+ - determine the sampling rate (compare the timestamps for the last 2 lines in the HK file);
350
+ - determine how many samples we need to read (starting at the back);
351
+ - read the required number of line, starting at the back;
352
+ - for each of the read lines, append the timestamp and HK value to the arrays that will be returned.
353
+ * Any other files: entire file will have to be read:
354
+ - determine the filename;
355
+ - read all lines;
356
+ - for each line: append the timestamp and HK value to the arrays that will be returned.
357
+
358
+ Args:
359
+ - hk_name: Name of the housekeeping parameter.
360
+ - data_dir: Folder (with sub-folders /daily and /obs) in which the HK files are stored.
361
+ - time_window: Length of the time window over which to retrieve the housekeeping [s]. The time window ends at
362
+ the moment this method is called. If not given, the latest HK-value is returned.
363
+ - setup: Setup.
364
+
365
+ Returns:
366
+ - If the time window has not been specified: the most recent timestamp and HK value.
367
+ - If the time window has been specified: an array of timestamps and an array of HK values, belonging to the
368
+ specified time window.
369
+ """
370
+
371
+ setup = setup or load_setup()
372
+ hk_dir = f"{data_dir}/daily/" # Where the HK is stored
373
+
374
+ try:
375
+
376
+ origin, timestamp_name = get_hk_info(hk_name, setup=setup)
377
+
378
+ except KeyError:
379
+
380
+ raise HKError(f"Cannot determine which EGSE component generated HK parameter {hk_name}")
381
+
382
+ # No time window specified: return the last value
383
+
384
+ if time_window is None:
385
+
386
+ # Look for the last file of this component
387
+
388
+ timestamp = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%d")
389
+ hk_dir += f"{timestamp}/"
390
+ filename = f"{timestamp}_{SITE_ID}_{origin}.csv"
391
+
392
+ timestamp_index, hk_index = get_indices(hk_dir + filename, hk_name, timestamp_name)
393
+ return get_last_non_empty(hk_dir + filename, timestamp_index, hk_index)
394
+
395
+ # Time window specified
396
+
397
+ else:
398
+
399
+ # We will return an array of timestamps and an array of HK values
400
+
401
+ timestamp_array = np.array([])
402
+ hk_array = np.array([])
403
+
404
+ # Go back in time from this very moment and determine:
405
+ # - which timespan the most recent HK file covers (i.e. how much time has elapsed since midnight)
406
+ # - what the time is at the start of the time window
407
+
408
+ now = datetime.datetime.utcnow()
409
+ elapsed_since_midnight = now.microsecond * 1e-6 + now.second + 60 * (now.minute + 60 * now.hour)
410
+ start_time = now - datetime.timedelta(seconds=time_window)
411
+ start_od = f"{start_time.year}{start_time.month:02d}{start_time.day:02d}"
412
+
413
+ # Determine which columns will be needed from which file
414
+
415
+ filename = f"{start_od}/{start_od}_{SITE_ID}_{origin}.csv"
416
+
417
+ if Path(hk_dir + filename).exists():
418
+
419
+ timestamp_index, hk_index = get_indices(hk_dir + filename, hk_name, timestamp_name)
420
+
421
+ # Determine how many time samples you need to read in the first relevant HK file (starting from the back)
422
+
423
+ sampling_rate = get_sampling_rate(hk_dir + filename, timestamp_name) # Time between subsequent samples
424
+
425
+ if time_window <= elapsed_since_midnight:
426
+
427
+ num_samples_first_day = int(round(time_window / sampling_rate))
428
+
429
+ else:
430
+
431
+ time_window_first_day = (time_window - elapsed_since_midnight) % SECONDS_IN_A_DAY
432
+ num_samples_first_day = int(round(time_window_first_day / sampling_rate)) # TODO Round or floor?
433
+
434
+ # Read the required number of lines in the relevant HK file (starting from the back of the file)
435
+
436
+ lines_first_day = read_last_lines(hk_dir + filename, num_samples_first_day)
437
+
438
+ for line in lines_first_day:
439
+
440
+ line = line.split(",")
441
+
442
+ timestamp_array = np.append(timestamp_array, line[timestamp_index])
443
+ hk_array = np.append(hk_array, line[hk_index])
444
+
445
+ # In case we also need to read more recent files
446
+ # (those will have to be read entirely)
447
+
448
+ else:
449
+
450
+ _LOGGER.warning(f"No HK available for {origin} on "
451
+ f"{start_time.day}/{start_time.month}/{start_time.year}")
452
+
453
+ day = (start_time + datetime.timedelta(days=1)).date() # The day after the first day
454
+ last_day = datetime.date(now.year, now.month, now.day) # Today
455
+
456
+ while day <= last_day:
457
+
458
+ od = f"{day.year}{day.month:02d}{day.day:02d}"
459
+ filename = f"{od}/{od}_{SITE_ID}_{origin}.csv"
460
+
461
+ if Path(hk_dir + filename).exists():
462
+
463
+ with open(hk_dir + filename) as file:
464
+
465
+ csv_reader = csv.reader(file)
466
+
467
+ header = next(csv_reader) # Skip the header
468
+ timestamp_index = header.index("timestamp")
469
+ try:
470
+ hk_index = header.index(hk_name)
471
+ except ValueError:
472
+ raise HKError(f"Cannot find column {hk_name} in {filename}")
473
+
474
+ for row in csv_reader:
475
+
476
+ timestamp_array = np.append(timestamp_array, row[timestamp_index])
477
+ hk_array = np.append(hk_array, row[hk_index])
478
+
479
+ else:
480
+
481
+ _LOGGER.warning(f"No HK available for {origin} on {day.day}/{day.month}/{day.year}")
482
+
483
+ day += datetime.timedelta(days=1)
484
+
485
+ for index in range(len(timestamp_array)):
486
+
487
+ timestamp_array[index] = time_since_epoch_1958(timestamp_array[index])
488
+
489
+ return timestamp_array, hk_array
490
+
491
+
492
+ def get_last_non_empty(filename: str, timestamp_index: int, hk_index: int):
493
+ """ Return the timestamp and HK value for last real value.
494
+
495
+ Args:
496
+ - filename: HK file in which to look for the given HK parameter.
497
+ - timestamp_index: Index of the column with the timestamps.
498
+ - hk_index: Index of the column with the HK parameter with the given name.
499
+
500
+ Returns: The timestamp and HK value with the last real value.
501
+ """
502
+
503
+ timestamp = None
504
+ hk_value = " "
505
+
506
+ filename = Path(filename)
507
+
508
+ if not filename.exists():
509
+ return None
510
+
511
+ # Declaring variable to implement exponential search
512
+
513
+ try:
514
+
515
+ num_lines = 1
516
+
517
+ while hk_value == " " or hk_value == "":
518
+
519
+ pos = num_lines + 1
520
+
521
+ # List to store last N lines
522
+
523
+ lines = []
524
+
525
+ with open(filename) as f:
526
+
527
+ while len(lines) <= num_lines:
528
+
529
+ try:
530
+
531
+ f.seek(-pos, 2)
532
+
533
+ except IOError:
534
+
535
+ f.seek(0)
536
+ break
537
+
538
+ finally:
539
+
540
+ lines = list(f)
541
+
542
+ # Increasing value of variable exponentially
543
+
544
+ pos *= 2
545
+
546
+ last_line = lines[-num_lines].rstrip("\r").split(",")
547
+ timestamp, hk_value = last_line[timestamp_index], last_line[hk_index]
548
+
549
+ num_lines += 1
550
+
551
+ return time_since_epoch_1958(timestamp), hk_value
552
+
553
+ except IndexError:
554
+ return None, None
555
+
556
+
557
+ def get_indices(filename: str, hk_name: str, timestamp_name: str):
558
+ """ Return the column number of the timestamp and given HK parameter in the given HK file.
559
+
560
+ Args:
561
+ - filename: HK file in which to look for the given HK parameter.
562
+ - hk_name: Name of the HK parameter.
563
+ - timestamp_name: Name of the corresponding timestamp.
564
+
565
+ Returns:
566
+ - Index of the column with the timestamps.
567
+ - Index of the column with the HK parameter with the given name.
568
+ """
569
+
570
+ with open(filename, "r") as f:
571
+
572
+ reader = csv.reader(f)
573
+ header = next(reader) # Skip the header
574
+
575
+ timestamp_index = header.index(timestamp_name)
576
+ # timestamp_index = 0
577
+
578
+ try:
579
+
580
+ hk_index = header.index(hk_name)
581
+
582
+ except ValueError:
583
+
584
+ raise HKError(f"Cannot find column {hk_name} in {filename}")
585
+
586
+ return timestamp_index, hk_index
587
+
588
+
589
+ def get_sampling_rate(filename: str, timestamp_name: str):
590
+ """ Return the sampling rate for the HK file with the given name [s].
591
+
592
+ The sampling rate is determine as the difference between the timestamps of the last two lines of the HK file.
593
+
594
+ Args:
595
+ - filename: Name of the HK file. We do not check explicitly whether this file exists.
596
+
597
+ Returns: Sampling rate for the HK file with the given name [s].
598
+ """
599
+
600
+ # Determine which column comprises the timestamp
601
+
602
+ with open(filename, "r") as f:
603
+
604
+ reader = csv.reader(f)
605
+ header = next(reader) # Skip the header
606
+
607
+ timestamp_index = header.index(timestamp_name)
608
+
609
+ # Read the last 2 lines and extract the timestamps for these lines
610
+
611
+ eof = read_last_lines(filename, 2)
612
+
613
+ penultimate_timestamp = date_parser.parse(eof[0].split(",")[timestamp_index])
614
+ last_timestamp = date_parser.parse(eof[1].split(",")[timestamp_index])
615
+
616
+ # Calculate the sampling rate [s]
617
+
618
+ return (last_timestamp - penultimate_timestamp).total_seconds()
619
+
620
+
621
+ def convert_hk_names(original_hk: dict, conversion_dict: dict) -> dict:
622
+ """
623
+ Converts the names of the HK parameters in the given dictionary.
624
+
625
+ The names/keys in the given dictionary of HK parameters (original_hk) are replaced by the names
626
+ from the given conversion dictionary. The original dictionary is left unchanged, a new dictionary is
627
+ returned.
628
+
629
+ Args:
630
+ - original_hk: Original dictionary of HK parameters.
631
+ - conversion_dict: Dictionary with the original HK names as keys and the new HK names as values.
632
+
633
+ Returns:
634
+ A new dictionary of HK parameters with the corrected HK names.
635
+ """
636
+
637
+ converted_hk = {}
638
+
639
+ for orig_key in original_hk:
640
+
641
+ try:
642
+ new_key = conversion_dict[orig_key]
643
+ except KeyError:
644
+ new_key = orig_key # no conversion, just copy the key:value pair
645
+
646
+ converted_hk[new_key] = original_hk[orig_key]
647
+
648
+ return converted_hk
649
+
650
+
651
+ def read_conversion_dict(storage_mnemonic: str, use_site: bool = False, setup: Optional[Setup] = None):
652
+ """ Read the HK spreadsheet and compose conversion dictionary for HK names.
653
+
654
+ The spreadsheet contains the following information:
655
+
656
+ - storage mnemonic of the component that generates the HK
657
+ - original HK name (as is comes from the device itself)
658
+ - HK name with the correct prefix
659
+ - name of the column (in the HK file) with the corresponding timestamp
660
+
661
+ Args:
662
+ - storage_mnemonic: Storage mnemonic of the component for which to compose the conversion dictionary
663
+ - use_site: Indicate whether the prefixes of the new HK names are TH-specific
664
+ - setup: Setup.
665
+
666
+ Returns: Dictionary with the original HK names as keys and the converted HK names as values.
667
+ """
668
+
669
+ setup = setup or load_setup()
670
+
671
+ try:
672
+ hk_info_table = setup.telemetry.dictionary
673
+ except AttributeError:
674
+ raise SetupError("Version of the telemetry dictionary not specified in the current setup")
675
+
676
+ storage_mnemonic_col = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
677
+ correct_name_col = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values
678
+ original_name_col = hk_info_table[TmDictionaryColumns.ORIGINAL_EGSE_HK_NAMES].values
679
+
680
+ selection = np.where(storage_mnemonic_col == storage_mnemonic)
681
+
682
+ correct_name_col = correct_name_col[selection]
683
+ original_name_col = original_name_col[selection]
684
+
685
+ if use_site:
686
+
687
+ th_prefix = f"G{SITE_ID}_"
688
+
689
+ th_conversion_dict = {}
690
+
691
+ for (original_name, correct_name) in zip(original_name_col, correct_name_col):
692
+ if str.startswith(str(correct_name), th_prefix):
693
+ th_conversion_dict[original_name] = correct_name
694
+
695
+ return th_conversion_dict
696
+
697
+ else:
698
+
699
+ if len(original_name_col) != len(correct_name_col):
700
+ _LOGGER.error(f"Name columns in TM dictionary have different length: "
701
+ f"{len(original_name_col)} != {len(correct_name_col)}")
702
+
703
+ return dict(zip(original_name_col, correct_name_col))
704
+
705
+
706
+ def get_hk_info(hk_name: str, setup: Optional[Setup] = None):
707
+ """ Read the HK spreadsheet and extract information for the given HK parameter.
708
+
709
+ The spreadsheet contains the following information:
710
+
711
+ - storage mnemonic of the component that generates the HK
712
+ - original HK name
713
+ - HK name with the correct prefix
714
+ - name of the column (in the HK file) with the corresponding timestamp
715
+
716
+ Args:
717
+ - hk_name: Name of the HK parameter.
718
+ - setup: Setup.
719
+
720
+ Returns:
721
+ - storage mnemonic of the component that generates the given HK parameter
722
+ - name of the column in the HK file with the corresponding timestamp
723
+ """
724
+
725
+ setup = setup or load_setup()
726
+ hk_info_table = setup.telemetry.dictionary
727
+
728
+ storage_mnemonic = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
729
+ hk_names = hk_info_table[TmDictionaryColumns.CORRECT_HK_NAMES].values
730
+ timestamp_col = hk_info_table[TmDictionaryColumns.TIMESTAMP_NAMES].values
731
+
732
+ selection = np.where(hk_names == hk_name)
733
+
734
+ try:
735
+ return storage_mnemonic[selection][0], timestamp_col[selection][0]
736
+ except IndexError:
737
+ raise HKError(f"HK parameter {hk_name} unknown")
738
+
739
+
740
+ def get_storage_mnemonics(setup: Setup = None):
741
+ """ Return the list of the storage mnemonics from the TM dictionary.
742
+
743
+ Args:
744
+ - setup: Setup.
745
+
746
+ Returns: List of the storage mnemonics from the TM dictionary.
747
+ """
748
+
749
+ setup = setup or load_setup()
750
+
751
+ hk_info_table = setup.telemetry.dictionary
752
+ storage_mnemonics = hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC].values
753
+
754
+ return np.unique(storage_mnemonics)
755
+
756
+
757
+ def get_housekeeping_names(name_filter=None, device_filter=None, setup: Setup = None):
758
+ """ Return HK names, storage mnemonic, and description.
759
+
760
+ The TM dictionary is read into a Pandas DataFrame. If a device filter is given, only the rows pertaining to the
761
+ given storage mnemonic are kept. If a name filter is given, only the rows for which the HK parameter name contains
762
+ the given name filter are kept.
763
+
764
+ The result is returned as a Pandas DataFrame with the following columns:
765
+ - "CAM EGSE mnemonic": Name of the HK parameter;
766
+ - "Storage mnemonic": Storage mnemonic of the device producing the HK;
767
+ - "Description": Description of the HK parameter.
768
+
769
+ Synopsis:
770
+ - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK")
771
+ - get_housekeeping_names(name_filter="RAW", device_filter="N-FEE-HK", setup=setup)
772
+
773
+ Args:
774
+ - name_filter: Filter the HK dataframe, based on (a part of) the name of the HK parameter(s)
775
+ - device: Filter the HK dataframe, based on the given storage mnemonic
776
+ - setup: Setup.
777
+
778
+ Returns: Pandas DataFrame with the HK name, storage mnemonic, and description of the HK parameters that pass the
779
+ given filter.
780
+ """
781
+
782
+ setup = setup or load_setup()
783
+
784
+ hk_info_table = setup.telemetry.dictionary
785
+ hk_info_table.dropna(subset=[TmDictionaryColumns.CORRECT_HK_NAMES], inplace=True)
786
+
787
+ if device_filter:
788
+ hk_info_table = hk_info_table.loc[hk_info_table[TmDictionaryColumns.STORAGE_MNEMONIC] == device_filter]
789
+
790
+ if name_filter:
791
+ hk_info_table = hk_info_table.query(f'`{TmDictionaryColumns.CORRECT_HK_NAMES}`.str.contains("{name_filter}")')
792
+
793
+ return hk_info_table[[TmDictionaryColumns.CORRECT_HK_NAMES, TmDictionaryColumns.STORAGE_MNEMONIC,
794
+ TmDictionaryColumns.DESCRIPTION]]