Source code for ska_low_mccs.subarray.qa_metrics_builder
# -*- coding: utf-8 -*-
#
# This file is part of the SKA Low MCCS project
#
#
# Distributed under the terms of the BSD 3-clause new license.
# See LICENSE for more info.
"""QA Metrics builder for subarray quality assurance metrics."""
from __future__ import annotations
import json
import logging
from importlib import resources
from typing import Any, Final
from jsonschema import ValidationError, validate
SUBARRAY_QA_SCHEMA: Final = json.loads(
resources.files("ska_low_mccs.schemas.subarray_beam")
.joinpath("MccsSubarrayBeam_QualityAssuranceMetrics_1_0.json")
.read_text()
)
[docs]
class QAMetricsBuilder:
"""Builder for constructing valid QA metrics structures."""
[docs]
def __init__(self, logger: logging.Logger) -> None:
"""
Initialize the builder.
:param logger: logger
"""
self._logger = logger
self._subarray_beams: dict[str, Any] = {}
self._beam_definitions: dict[str, list[str]] = {}
self._total_beams = 0
[docs]
def define_beam(
self,
beam_id: str,
aperture_ids: list[str],
) -> QAMetricsBuilder:
"""
Add a beam to the metrics.
:param beam_id: Identifier for the beam
:param aperture_ids: List of aperture IDs for the beam
:return: Self for method chaining
"""
is_new = not self.has_beam(beam_id)
if is_new:
self._total_beams += 1
self._beam_definitions[beam_id] = aperture_ids
return self
[docs]
def has_beam(self, beam_id: str) -> bool:
"""
Return of beam already defined.
:param beam_id: the beam id
:return: true if already defined.
"""
return beam_id in self._beam_definitions
[docs]
def update_beam_metrics(
self,
beam_id: str,
beam_qa_info: dict[str, dict[str, Any]],
) -> QAMetricsBuilder:
"""
Update metrics for a beam.
:param beam_id: Identifier for the beam
:param beam_qa_info: QA information for the beam
:return: Self for method chaining
"""
if beam_id not in self._beam_definitions:
self._logger.warning(f"Ignoring metrics for undefined beam {beam_id}")
return self
try:
validate(instance=beam_qa_info, schema=SUBARRAY_QA_SCHEMA)
except ValidationError as e:
self._logger.error(f"Invalid beam QA info for {beam_id}: {e}")
return self
self._subarray_beams[beam_id] = beam_qa_info
return self
[docs]
def build(self) -> dict[str, Any]:
"""
Build the complete QA metrics dictionary.
:return: Valid QA metrics structure
"""
locked_percent = self._calculate_locked_beam_percent()
return {
"subarray_beams": self._subarray_beams,
"beam_locked_percent": locked_percent,
}
def _calculate_locked_beam_percent(self: QAMetricsBuilder) -> float:
# Collect all allocated beam locked statuses
locked_beam_statuses = []
for beam_id, beam_qa in self._subarray_beams.items():
is_beam_locked = beam_qa.get("is_beam_locked", False)
locked_beam_statuses.append(is_beam_locked)
# Calculate percentage
if locked_beam_statuses:
locked_count = sum(locked_beam_statuses)
percent_locked = (locked_count / len(locked_beam_statuses)) * 100
self._logger.debug(
"Calculated locked percent: "
f"{locked_count}/{len(locked_beam_statuses)} "
f"beams locked = {percent_locked}%"
)
else:
percent_locked = 0.0
self._logger.debug("No beam metrics available, locked percent = 0.0%")
return percent_locked
[docs]
def reset(self) -> QAMetricsBuilder:
"""
Reset the builder for reuse.
:return: Self for method chaining
"""
self._subarray_beams = {}
self._beam_definitions = {}
self._total_beams = 0
return self