Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[draft] handle differential calibration correction #113

Closed
wants to merge 66 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
b200f55
rename Calibration to SensorCalibration
aromanielloNTIA Jan 19, 2024
367e4db
refactor calibration to use a base class
aromanielloNTIA Jan 22, 2024
9f68928
fix tests, add sensor_uid to SensorCalibration
aromanielloNTIA Jan 22, 2024
d596875
Merge branch 'discover_action_types' into calibrate_to_antenna
aromanielloNTIA Jan 22, 2024
e4234a9
remove sigan calibrations
aromanielloNTIA Jan 22, 2024
3be9f69
update docstring for 'update' method
aromanielloNTIA Jan 22, 2024
50b596f
add differential calibration dataclass
aromanielloNTIA Jan 22, 2024
b8ad5d0
add field input validator to base class
aromanielloNTIA Jan 22, 2024
971409b
add to_json method
aromanielloNTIA Jan 22, 2024
96488c6
use to_json in update
aromanielloNTIA Jan 22, 2024
c1a5f7d
update calibration-related unit tests
aromanielloNTIA Jan 22, 2024
c304e02
bump version number
aromanielloNTIA Jan 22, 2024
fc2d450
remove unused imports
aromanielloNTIA Jan 22, 2024
6d30ad2
Merge branch 'discover_action_types' into calibrate_to_antenna
aromanielloNTIA Jan 23, 2024
013b791
remove unused imports
aromanielloNTIA Jan 23, 2024
ddac9c5
simplify expressions in Y-factor cal
aromanielloNTIA Jan 24, 2024
f978515
Use "loss" as value key instead of "differential_loss"
aromanielloNTIA Jan 24, 2024
0832db6
clarify functionality of _validate_fields
aromanielloNTIA Jan 24, 2024
49503f7
implement sensor-level acquire_samples
aromanielloNTIA Jan 24, 2024
4a81a2a
remove unused import
aromanielloNTIA Jan 24, 2024
43ac4be
update actions for sensor cal handling changes
aromanielloNTIA Jan 24, 2024
eba790a
generalize differential calibration module docstring
aromanielloNTIA Jan 24, 2024
fdea1a7
make calibration data a sensor property
aromanielloNTIA Jan 24, 2024
3dfdb52
implement mock sensor for testing
aromanielloNTIA Jan 25, 2024
89e5ae7
simplify and update mock sigan
aromanielloNTIA Jan 25, 2024
59b87ca
Make firmware and API version properties usable
aromanielloNTIA Jan 25, 2024
e756195
Update test_sigan.py
aromanielloNTIA Jan 25, 2024
f1b1a63
generalize matching of calibration params
aromanielloNTIA Jan 25, 2024
087a3c1
update DifferentialCalibration docstring
aromanielloNTIA Jan 25, 2024
5c99a97
make calibration_reference required for all calibrations
aromanielloNTIA Jan 25, 2024
074bce2
Merge branch 'master' into calibrate_to_antenna
aromanielloNTIA Feb 7, 2024
4990d2e
fail early if sensor has no calibration object
aromanielloNTIA Feb 7, 2024
17595fa
fix missing indexing key
aromanielloNTIA Feb 7, 2024
2ddaecc
Update .pre-commit-config.yaml
aromanielloNTIA Feb 26, 2024
88a6b32
fix log message when checking integer keys
aromanielloNTIA Feb 26, 2024
382e907
add debug mesasges for testing
aromanielloNTIA Feb 26, 2024
89313d8
remove is_default calibration parameter
aromanielloNTIA Mar 5, 2024
7851493
Do not overwrite sensor calibration file
aromanielloNTIA Mar 5, 2024
51820e8
fix environment variable reference
aromanielloNTIA Mar 5, 2024
afbf2bf
add missing import
aromanielloNTIA Mar 5, 2024
8bddcf2
fix from_json calibration file tests
aromanielloNTIA Mar 5, 2024
c36efd8
remove unused imports
aromanielloNTIA Mar 5, 2024
57a3dca
fix cal data lookup unit tests
aromanielloNTIA Mar 5, 2024
3d52f8e
fix formatting in cal lookup error
aromanielloNTIA Mar 5, 2024
28ec1b0
fix calibration from_json unit tests
aromanielloNTIA Mar 6, 2024
20845a5
fix kwargs in call to parent class init
aromanielloNTIA Mar 6, 2024
3fe4adc
fix incorrect attribute name in unit test
aromanielloNTIA Mar 6, 2024
f901514
remove tests for no longer used sigan attribute
aromanielloNTIA Mar 6, 2024
ab2831d
avoid error due to debug messages when run with no calibration
aromanielloNTIA Mar 6, 2024
71dcd85
fix generator return type hint
aromanielloNTIA Mar 6, 2024
4f33c3e
don't require datetime in sensor cal data
aromanielloNTIA Mar 6, 2024
1c6f700
Fill metadata reference from loaded calibration
aromanielloNTIA Mar 12, 2024
e849a0d
Correct ONBOARD_CALIBRATION_FILE var.
dboulware Mar 14, 2024
b17b749
Use Path for file_path.
dboulware Mar 14, 2024
4e22e83
debug
dboulware Mar 14, 2024
d9d9bb4
create global data product metadata after first IQ capture so referen…
dboulware Mar 14, 2024
8012c2b
Add expired method to SensorCalibration.
dboulware Mar 14, 2024
9a1004e
set CALIBRATION_EXPIRATION_LIMIT to None if not set.
dboulware Mar 14, 2024
bdb142b
recusive check if calibration has expired and associated tests.
dboulware Mar 14, 2024
16366c7
pre-commit
dboulware Mar 14, 2024
15d63d8
get CALIBRATION_EXPIRATION_LIMIT as int.
dboulware Mar 14, 2024
aab77b9
Merge branch 'master' of https://github.com/NTIA/scos-actions into ca…
dboulware Mar 14, 2024
57452a6
move import of ray into __call__ just in case.
dboulware Mar 14, 2024
521518a
Restore import or ray.
dboulware Mar 14, 2024
23faff1
Add logging.
dboulware Mar 14, 2024
838b950
Additional debug logging.
dboulware Mar 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
rev: v3.15.1
hooks:
- id: pyupgrade
args: ["--py38-plus"]
Expand All @@ -30,12 +30,12 @@ repos:
types: [file, python]
args: ["--profile", "black", "--filter-files", "--gitignore"]
- repo: https://github.com/psf/black
rev: 23.12.1
rev: 24.2.0
hooks:
- id: black
types: [file, python]
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.38.0
rev: v0.39.0
hooks:
- id: markdownlint
types: [file, markdown]
Expand Down
1 change: 1 addition & 0 deletions sample_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This is a sample file showing how an action be created and called for debugging purposes
using a mock signal analyzer.
"""

import json

from scos_actions.actions.acquire_single_freq_fft import SingleFrequencyFftAcquisition
Expand Down
2 changes: 1 addition & 1 deletion scos_actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "8.0.1"
__version__ = "9.0.0"
59 changes: 36 additions & 23 deletions scos_actions/actions/acquire_sea_data_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
import logging
import lzma
import platform
import ray
import sys
from enum import EnumMeta
from time import perf_counter
from typing import Tuple

import numpy as np
import psutil
import ray
from environs import Env
from its_preselector import __version__ as PRESELECTOR_API_VERSION
from scipy.signal import sos2tf, sosfilt
Expand Down Expand Up @@ -109,7 +109,6 @@
FFT_WINDOW = get_fft_window(FFT_WINDOW_TYPE, FFT_SIZE)
FFT_WINDOW_ECF = get_fft_window_correction(FFT_WINDOW, "energy")
IMPEDANCE_OHMS = 50.0
DATA_REFERENCE_POINT = "noise source output"
NUM_ACTORS = 3 # Number of ray actors to initialize

# Create power detectors
Expand Down Expand Up @@ -422,7 +421,7 @@ def __init__(self, params: dict, iir_sos: np.ndarray):
]
del params

def run(self, iqdata: np.ndarray) -> list:
def run(self, iqdata: np.ndarray):
"""
Filter the input IQ data and concurrently compute FFT, PVT, PFP, and APD results.

Expand All @@ -449,6 +448,7 @@ class NasctnSeaDataProduct(Action):
def __init__(self, parameters: dict):
super().__init__(parameters)
# Assume preselector is present
self.total_channel_data_length = None
rf_path_name = utils.get_parameter(RF_PATH, self.parameters)
self.rf_path = {self.PRESELECTOR_PATH_KEY: rf_path_name}

Expand Down Expand Up @@ -506,6 +506,7 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
action_start_tic = perf_counter()
# Ray should have already been initialized within scos-sensor,
# but check and initialize just in case.

if not ray.is_initialized():
logger.info("Initializing ray.")
logger.info("Set RAY_INIT=true to avoid initializing within " + __name__)
Expand All @@ -525,8 +526,6 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
self.iteration_params,
)
self.create_global_sensor_metadata(self.sensor)
self.create_global_data_product_metadata()

# Initialize remote supervisor actors for IQ processing
tic = perf_counter()
# This uses iteration_params[0] because
Expand All @@ -538,10 +537,15 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
logger.debug(f"Spawned {NUM_ACTORS} supervisor actors in {toc-tic:.2f} s")

# Collect all IQ data and spawn data product computation processes
dp_procs, cpu_speed = [], []
dp_procs, cpu_speed, reference_points = [], [], []
capture_tic = perf_counter()

for i, parameters in enumerate(self.iteration_params):
measurement_result = self.capture_iq(parameters)
if i == 0:
self.create_global_data_product_metadata(
measurement_result["reference"]
)
# Start data product processing but do not block next IQ capture
tic = perf_counter()

Expand All @@ -552,16 +556,22 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
toc = perf_counter()
logger.debug(f"IQ data delivered for processing in {toc-tic:.2f} s")
# Create capture segment with channel-specific metadata before sigan is reconfigured
tic = perf_counter()
self.create_capture_segment(i, measurement_result)
toc = perf_counter()
logger.debug(f"Created capture metadata in {toc-tic:.2f} s")
# Query CPU speed for later averaging in diagnostics metadata
cpu_speed.append(get_current_cpu_clock_speed())
# Append list of data reference points; later we require these to be identical
reference_points.append(measurement_result["reference"])
capture_toc = perf_counter()
logger.debug(
f"Collected all IQ data and started all processing in {capture_toc-capture_tic:.2f} s"
)

# Create data product metadata: requires all data reference points
# to be identical.
assert (
len(set(reference_points)) == 1
), "Channel data were scaled to different reference points. Cannot build metadata."

# Collect processed data product results
all_data, max_max_ch_pwrs, med_mean_ch_pwrs, mean_ch_pwrs, median_ch_pwrs = (
[],
Expand Down Expand Up @@ -630,14 +640,11 @@ def capture_iq(self, params: dict) -> dict:
nskip = utils.get_parameter(NUM_SKIP, params)
num_samples = int(params[SAMPLE_RATE] * duration_ms * 1e-3)
# Collect IQ data
measurement_result = self.sensor.signal_analyzer.acquire_time_domain_samples(
num_samples, nskip
measurement_result = self.sensor.acquire_time_domain_samples(
num_samples, nskip, cal_params=params
)
# Store some metadata with the IQ
measurement_result.update(params)
measurement_result[
"sensor_cal"
] = self.sensor.signal_analyzer.sensor_calibration_data
toc = perf_counter()
logger.debug(
f"IQ Capture ({duration_ms} ms @ {(params[FREQUENCY]/1e6):.1f} MHz) completed in {toc-tic:.2f} s."
Expand Down Expand Up @@ -978,7 +985,7 @@ def test_required_components(self):
trigger_api_restart.send(sender=self.__class__)
return None

def create_global_data_product_metadata(self) -> None:
def create_global_data_product_metadata(self, data_products_reference: str) -> None:
p = self.parameters
num_iq_samples = int(p[SAMPLE_RATE] * p[DURATION_MS] * 1e-3)
iir_obj = ntia_algorithm.DigitalFilter(
Expand Down Expand Up @@ -1023,7 +1030,7 @@ def create_global_data_product_metadata(self) -> None:
x_step=[p[SAMPLE_RATE] / FFT_SIZE],
y_units="dBm/Hz",
processing=[dft_obj.id],
reference=DATA_REFERENCE_POINT,
reference=data_products_reference,
description=(
"Results of statistical detectors (max, mean, median, 25th_percentile, 75th_percentile, "
+ "90th_percentile, 95th_percentile, 99th_percentile, 99.9th_percentile, 99.99th_percentile) "
Expand All @@ -1043,7 +1050,7 @@ def create_global_data_product_metadata(self) -> None:
x_stop=[pvt_x_axis__s[-1]],
x_step=[pvt_x_axis__s[1] - pvt_x_axis__s[0]],
y_units="dBm",
reference=DATA_REFERENCE_POINT,
reference=data_products_reference,
description=(
"Max- and mean-detected channel power vs. time, with "
+ f"an integration time of {p[TD_BIN_SIZE_MS]} ms. "
Expand All @@ -1070,7 +1077,7 @@ def create_global_data_product_metadata(self) -> None:
x_stop=[pfp_x_axis__s[-1]],
x_step=[pfp_x_axis__s[1] - pfp_x_axis__s[0]],
y_units="dBm",
reference=DATA_REFERENCE_POINT,
reference=data_products_reference,
description=(
"Channelized periodic frame power statistics reported over"
+ f" a {p[PFP_FRAME_PERIOD_MS]} ms frame period, with frame resolution"
Expand All @@ -1093,6 +1100,7 @@ def create_global_data_product_metadata(self) -> None:
y_start=[apd_y_axis__dBm[0]],
y_stop=[apd_y_axis__dBm[-1]],
y_step=[apd_y_axis__dBm[1] - apd_y_axis__dBm[0]],
reference=data_products_reference,
description=(
f"Estimate of the APD, using a {p[APD_BIN_SIZE_DB]} dB "
+ "bin size for amplitude values. The data payload includes"
Expand All @@ -1111,6 +1119,7 @@ def create_global_data_product_metadata(self) -> None:
+ pfp_length * len(PFP_M3_DETECTOR) * 2
+ apd_graph.length
)
logger.debug(f"Total channel length:{self.total_channel_data_length}")

def create_capture_segment(
self,
Expand All @@ -1126,11 +1135,15 @@ def create_capture_segment(
duration=measurement_result[DURATION_MS],
overload=measurement_result["overload"],
sensor_calibration=ntia_sensor.Calibration(
datetime=measurement_result["sensor_cal"]["datetime"],
gain=round(measurement_result["sensor_cal"]["gain"], 3),
noise_figure=round(measurement_result["sensor_cal"]["noise_figure"], 3),
temperature=round(measurement_result["sensor_cal"]["temperature"], 1),
reference=DATA_REFERENCE_POINT,
datetime=self.sensor.sensor_calibration_data["datetime"],
gain=round(measurement_result["applied_calibration"]["gain"], 3),
noise_figure=round(
measurement_result["applied_calibration"]["noise_figure"], 3
),
temperature=round(
self.sensor.sensor_calibration_data["temperature"], 1
),
reference=measurement_result["reference"],
),
sigan_settings=ntia_sensor.SiganSettings(
reference_level=self.sensor.signal_analyzer.reference_level,
Expand Down
15 changes: 5 additions & 10 deletions scos_actions/actions/acquire_single_freq_fft.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@
from numpy import float32, ndarray

from scos_actions.actions.interfaces.measurement_action import MeasurementAction
from scos_actions.hardware.mocks.mock_gps import MockGPS
from scos_actions.metadata.structs import ntia_algorithm
from scos_actions.signal_processing.fft import (
get_fft,
Expand Down Expand Up @@ -153,10 +152,6 @@ def __init__(self, parameters: dict):
self.classification = get_parameter(CLASSIFICATION, self.parameters)
self.cal_adjust = get_parameter(CAL_ADJUST, self.parameters)
assert isinstance(self.cal_adjust, bool)
if self.cal_adjust:
self.data_reference = "calibration terminal"
else:
self.data_reference = "signal analyzer input"
# FFT setup
self.fft_detector = create_statistical_detector(
"M4sDetector", ["min", "max", "mean", "median", "sample"]
Expand All @@ -169,7 +164,7 @@ def __init__(self, parameters: dict):
def execute(self, schedule_entry: dict, task_id: int) -> dict:
# Acquire IQ data and generate M4S result
measurement_result = self.acquire_data(
self.num_samples, self.nskip, self.cal_adjust
self.num_samples, self.nskip, self.cal_adjust, cal_params=self.parameters
)
# Actual sample rate may differ from configured value
sample_rate_Hz = measurement_result["sample_rate"]
Expand All @@ -178,9 +173,9 @@ def execute(self, schedule_entry: dict, task_id: int) -> dict:
# Save measurement results
measurement_result["data"] = m4s_result
measurement_result.update(self.parameters)
measurement_result[
"calibration_datetime"
] = self.sensor.signal_analyzer.sensor_calibration_data["datetime"]
# measurement_result["calibration_datetime"] = (
# self.sensor.sensor_calibration_data["datetime"]
# )
measurement_result["task_id"] = task_id
measurement_result["classification"] = self.classification

Expand Down Expand Up @@ -270,7 +265,7 @@ def create_metadata(self, measurement_result: dict, recording: int = None) -> No
x_stop=[frequencies[-1]],
x_step=[frequencies[1] - frequencies[0]],
y_units="dBm",
reference=self.data_reference,
reference=measurement_result["reference"],
description=(
"Results of min, max, mean, and median statistical detectors, "
+ f"along with a random sampling, from a set of {self.nffts} "
Expand Down
11 changes: 6 additions & 5 deletions scos_actions/actions/acquire_single_freq_tdomain_iq.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@

from scos_actions import utils
from scos_actions.actions.interfaces.measurement_action import MeasurementAction
from scos_actions.hardware.mocks.mock_gps import MockGPS
from scos_actions.utils import get_parameter

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -84,14 +83,16 @@ def execute(self, schedule_entry: dict, task_id: int) -> dict:
# Use the sigan's actual reported instead of requested sample rate
sample_rate = self.sensor.signal_analyzer.sample_rate
num_samples = int(sample_rate * self.duration_ms * 1e-3)
measurement_result = self.acquire_data(num_samples, self.nskip, self.cal_adjust)
measurement_result = self.acquire_data(
num_samples, self.nskip, self.cal_adjust, cal_params=self.parameters
)
end_time = utils.get_datetime_str_now()
measurement_result.update(self.parameters)
measurement_result["end_time"] = end_time
measurement_result["task_id"] = task_id
measurement_result[
"calibration_datetime"
] = self.sensor.signal_analyzer.sensor_calibration_data["datetime"]
# measurement_result["calibration_datetime"] = (
# self.sensor.sensor_calibration_data["datetime"]
# )
measurement_result["classification"] = self.classification
sigan_settings = self.get_sigan_settings(measurement_result)
logger.debug(f"sigan settings:{sigan_settings}")
Expand Down
22 changes: 10 additions & 12 deletions scos_actions/actions/acquire_stepped_freq_tdomain_iq.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
cal_adjust = get_parameter(CAL_ADJUST, measurement_params)
sample_rate = self.sensor.signal_analyzer.sample_rate
num_samples = int(sample_rate * duration_ms * 1e-3)
measurement_result = super().acquire_data(num_samples, nskip, cal_adjust)
measurement_result = super().acquire_data(
num_samples, nskip, cal_adjust, cal_params=measurement_params
)
measurement_result.update(measurement_params)
end_time = utils.get_datetime_str_now()
measurement_result["end_time"] = end_time
Expand All @@ -117,22 +119,18 @@ def __call__(self, sensor: Sensor, schedule_entry: dict, task_id: int):
overload=measurement_result["overload"],
sigan_settings=sigan_settings,
)
sigan_cal = self.sensor.signal_analyzer.sigan_calibration_data
sensor_cal = self.sensor.signal_analyzer.sensor_calibration_data
if sigan_cal is not None:
if "1db_compression_point" in sigan_cal:
sigan_cal["compression_point"] = sigan_cal.pop(
"1db_compression_point"
)
capture_segment.sigan_calibration = ntia_sensor.Calibration(**sigan_cal)
sensor_cal = self.sensor.sensor_calibration_data
if sensor_cal is not None:
if "1db_compression_point" in sensor_cal:
sensor_cal["compression_point"] = sensor_cal.pop(
"1db_compression_point"
)
capture_segment.sensor_calibration = ntia_sensor.Calibration(
**sensor_cal
)
if "reference" not in sensor_cal:
# If the calibration data already includes this, don't overwrite
sensor_cal["reference"] = measurement_result["reference"]
capture_segment.sensor_calibration = ntia_sensor.Calibration(
**sensor_cal
)
measurement_result["capture_segment"] = capture_segment

self.create_metadata(measurement_result, recording_id)
Expand Down
Loading
Loading