diff --git a/docs/src/index.rst b/docs/src/index.rst index 54d6f8857a3b7985a732d17d1fad09e6f4861bcd..414dec0786c8212470165be8154ebc137b5ccf4a 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -43,6 +43,7 @@ :hidden: ska_csp_configure + ska_csp_assignres ska_sdp_assignres ska_sdp_configure diff --git a/docs/src/ska_csp_assignres.rst b/docs/src/ska_csp_assignres.rst new file mode 100644 index 0000000000000000000000000000000000000000..3c9d0076b04c04ff3dfc8b9d9a1a4ac7f06bb3ff --- /dev/null +++ b/docs/src/ska_csp_assignres.rst @@ -0,0 +1,22 @@ + +ska-csp-assignresources +======================= + +.. ska-schema:: https://schema.skatelescope.org/ska-csp-assignresources/0.0 + :auto_reference: + :auto_target: + :lift_description: + :lift_definitions: + :lift_title: + + .. ska-schema-example:: https://schema.skatelescope.org/ska-csp-assignresources/0.0 + + Example (assignment with neither pulsar beams nor zooms) + + .. ska-schema-example:: https://schema.skatelescope.org/ska-csp-assignresources/0.0 pst + + Example (assignment with PST beams) + + .. ska-schema-example:: https://schema.skatelescope.org/ska-csp-assignresources/0.0 zoom + + Example (assignment with zooms) diff --git a/src/ska_telmodel/_common.py b/src/ska_telmodel/_common.py index f105f7280b8df9882130b81cbe31e48a8757403c..a9f9224bc121f84a612781de680bfe9eb52098f7 100644 --- a/src/ska_telmodel/_common.py +++ b/src/ska_telmodel/_common.py @@ -1,7 +1,13 @@ +from enum import IntEnum from typing import Any, Callable, Tuple from schema import Schema, Regex, And, Optional +class TELESCOPE(IntEnum): + LOW = (0,) + MID = 1 + + def make(name: str, version: int, strict: bool, schema_input: Any, **kwargs) -> Schema: """Simple wrapper around "Schema" @@ -27,7 +33,6 @@ def mk_if(cond: bool) -> Callable[[Any], Any]: def get_channel_map_schema(elem_type: Any, version: int, strict: bool) -> Schema: - elem_schema = Schema(elem_type) def valid_channel_map_entry(entry): diff --git a/src/ska_telmodel/csp/examples.py b/src/ska_telmodel/csp/examples.py index 71a896a4d72655887630731e3d50a49955cea562..e0b63d01e78ce18eaf4b4046390682018dbf97d1 100644 --- a/src/ska_telmodel/csp/examples.py +++ b/src/ska_telmodel/csp/examples.py @@ -211,6 +211,76 @@ CSP_CONFIG_SCIENCE_A_0_1 = { ], } +CSP_ASSIGN_LOWCBF_0_0 = { + "interface": "https://schema.skatelescope.org/ska-csp-assignresources/0.0", + "common": {"subarrayID": 1}, + "lowcbf": { + "stations": [ + {"station_id": 1, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 2}, + ], + "station_beams": [ + { + "station_beam_id": 1, + "channels": [1, 2, 3, 4, 5, 6, 7, 8], + }, + { + "station_beam_id": 2, + "channels": [9, 10, 11, 12, 13, 14, 15], + }, + ], + }, +} + +CSP_ASSIGN_LOWCBF_PST_0_0 = { + "interface": "https://schema.skatelescope.org/ska-csp-assignresources/0.0", + "common": {"subarrayID": 1}, + "lowcbf": { + "stations": [ + {"station_id": 1, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 2}, + ], + "station_beams": [ + { + "station_beam_id": 1, + "channels": [1, 2, 3, 4, 5, 6, 7, 8], + "pst_beams": [{"pst_beam_id": 1}, {"pst_beam_id": 2}], + }, + { + "station_beam_id": 2, + "channels": [9, 10, 11, 12, 13, 14, 15], + "pst_beams": [{"pst_beam_id": 3}], + }, + ], + }, +} + +CSP_ASSIGN_LOWCBF_ZOOM_0_0 = { + "interface": "https://schema.skatelescope.org/ska-csp-assignresources/0.0", + "common": {"subarrayID": 1}, + "lowcbf": { + "stations": [ + {"station_id": 1, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 1}, + {"station_id": 3, "sub_station_id": 2}, + ], + "station_beams": [ + { + "station_beam_id": 1, + "channels": [1, 2, 3, 4, 5, 6, 7, 8], + "zooms": [{"zoom_id": 1}, {"zoom_id": 2}], + }, + { + "station_beam_id": 2, + "channels": [9, 10, 11, 12, 13, 14, 15], + "zooms": [{"zoom_id": 3}], + }, + ], + }, +} + def get_csp_config_example(version: str, scan: str = None) -> dict: """Generate examples for CSP configuration strings @@ -238,3 +308,22 @@ def get_csp_config_example(version: str, scan: str = None) -> dict: return copy.deepcopy(CSP_CONFIG_SCIENCE_A_1_0) raise ValueError(f"Could not generate example for schema {version} scan {scan}!") + + +def get_csp_assignres_example(version: str, assignment: str = None) -> dict: + """Generate examples for Assign Resources + + :param version: Version URI of configuration format + :param assignment: Which demo data? Valid selections: "pst", "zoom" + """ + if version.startswith(CSP_ASSIGNRES_VER0_0): + if assignment is None: + return copy.deepcopy(CSP_ASSIGN_LOWCBF_0_0) + elif assignment == "pst": + return copy.deepcopy(CSP_ASSIGN_LOWCBF_PST_0_0) + elif assignment == "zoom": + return copy.deepcopy(CSP_ASSIGN_LOWCBF_ZOOM_0_0) + + raise ValueError( + f"Could not generate example for schema {version} assignment {assignment}!" + ) diff --git a/src/ska_telmodel/csp/schema.py b/src/ska_telmodel/csp/schema.py index d8d0cbda2d03f6e9e8d6138cd519179ce313d5eb..5523001738fdf14c4a4d0f140fe20cbe66afac12 100644 --- a/src/ska_telmodel/csp/schema.py +++ b/src/ska_telmodel/csp/schema.py @@ -2,8 +2,9 @@ Used for checking CSP configuration strings for conformance """ -from schema import Schema, Optional, And, Or, Literal, Regex +from schema import Schema, Optional, And, Or, Literal, Regex, Forbidden from .. import _common +from .._common import TELESCOPE from . import version as csp_version from inspect import cleandoc @@ -207,12 +208,15 @@ def get_fsp_config_schema(version: str, strict: bool): return _common.make("FSP config", version, strict, elems, as_reference=True) -def get_cbf_config_schema(version: str, strict: bool) -> Schema: +def get_cbf_config_schema( + version: str, strict: bool, telescope: TELESCOPE = None +) -> Schema: """Central Beam Former configuration schema :param version: Interface Version URI :param strict: Schema strictness - :return: the JSON Schema for the MID.CBF configuration. + :param telescope: Low or Mid Telescope (enum) + :return: the JSON Schema for CBF configuration. """ # Sub-schemas @@ -398,6 +402,52 @@ def get_search_window_config_schema(version: str, strict: bool) -> Schema: ) +def get_cbf_assignres_schema( + version: str, strict: bool, telescope: TELESCOPE +) -> Schema: + """CBF Subarray AssignResources schema + + :param version: Interface Version URI + :param strict: Schema strictness + :param telescope: Low or Mid Telescope (enum) + :return: the JSON Schema for CBF configuration. + """ + if telescope == TELESCOPE.LOW: + return _common.make( + "Low CBF Assign Resources", + version, + strict, + { + "stations": [{"station_id": int, "sub_station_id": int}], + "station_beams": Or( + # either: (optional) beams with no zooms, or zooms with no beams + [ + { + "station_beam_id": int, + "channels": [int], + Optional("pss_beams"): [{"pss_beam_id": int}], + Optional("pst_beams"): [{"pst_beam_id": int}], + Forbidden("zooms"): object, + } + ], + [ + { + "station_beam_id": int, + "channels": [int], + "zooms": [{"zoom_id": int}], + Forbidden("pss_beams"): object, + Forbidden("pst_beams"): object, + } + ], + ), + }, + ) + else: + raise NotImplementedError( + "No subarray assign schema for telescope {}".format(telescope) + ) + + def get_subarray_config_schema(version: str, strict: bool) -> Schema: """CSP Subarray configuration schema @@ -431,7 +481,39 @@ def get_subarray_config_schema(version: str, strict: bool) -> Schema: ) -def get_common_config_schema(version: str, strict: bool) -> Schema: +def get_common_assignres_schema(version: str, strict: bool) -> Schema: + """CSP Subarray common assign resources schema. + + :param version: Interface Version URI + :param strict: Schema strictness + :return: the JSON Schema for the CSP subarray common configuration (ADR-18). + """ + + # Elements + elems = { + Optional("id"): str, + Literal("subarrayID", description=cleandoc("""Subarray number""")): int, + } + + # all subelements common schema + return _common.make( + "Common CSP assign resources", + version, + strict, + elems, + description=cleandoc( + """ + Common section, containing the parameters and the sections + belonging to all CSP sub elements. This sections is forwarded to + all sub-elements. """ + ), + as_reference=True, + ) + + +def get_common_config_schema( + version: str, strict: bool, telescope: TELESCOPE = None +) -> Schema: """CSP Subarray common configuration schema. :param version: Interface Version URI @@ -543,16 +625,19 @@ def get_pst_config_schema(version: str, strict: bool) -> Schema: ) -def get_csp_config_schema(version: str, strict: bool) -> Schema: +def get_csp_config_schema( + version: str, strict: bool, telescope: TELESCOPE = None +) -> Schema: """ Returns a schema to verify a CSP configuration :param version: Interface version :param strict: Strict mode - refuse even harmless schema violations (like extra keys). DO NOT USE FOR INPUT VALIDATION! + :param telescope: Low or Mid telescope (defaults to Mid's behaviour for backward compatibility, + suggest removing this default behaviour for clarity in future) :return: The JSON Schema for the CSP configuration. - :raise: `ValueError` exception on mismatch major version or invalid JSON - Schema URI + :raise: `ValueError` exception on mismatch major version or invalid JSON Schema URI """ # Which version? @@ -568,6 +653,9 @@ def get_csp_config_schema(version: str, strict: bool) -> Schema: return _common.make(name, version, strict, common_schema.schema) elif version.startswith(csp_version.CSP_CONFIG_VER1): + cbf_label = "cbf" + # if telescope == TELESCOPE.LOW: + # cbf_label = "lowcbf" # Overall configuration schema return _common.make( @@ -578,7 +666,7 @@ def get_csp_config_schema(version: str, strict: bool) -> Schema: Optional("interface"): str, "subarray": get_subarray_config_schema(version, strict), "common": get_common_config_schema(version, strict), - "cbf": get_cbf_config_schema(version, strict), + cbf_label: get_cbf_config_schema(version, strict, telescope), Optional("pss"): get_pss_config_schema(version, strict), Optional("pst"): get_pst_config_schema(version, strict), }, @@ -590,3 +678,50 @@ def get_csp_config_schema(version: str, strict: bool) -> Schema: else: raise ValueError(f"Invalid JSON Schema URI {version}") + + +def get_csp_assignres_schema( + version: str, strict: bool, telescope: TELESCOPE = TELESCOPE.LOW +) -> Schema: + """ + Returns a schema to verify a CSP resource assignment + + :param version: Interface version + :param strict: Strict mode - refuse even harmless schema + violations (like extra keys). DO NOT USE FOR INPUT VALIDATION! + :param telescope: Low or Mid telescope (TELESCOPE enum) + :return: The JSON Schema for the CSP configuration. + :raise: `ValueError` exception on mismatch major version or invalid JSON Schema URI + """ + + version = csp_version.get_csp_assignres_version(version) + if version.startswith(csp_version.CSP_ASSIGNRES_PREFIX): + + name = "CSP assign resources" + if version.startswith(csp_version.CSP_ASSIGNRES_VER0): + if telescope == TELESCOPE.LOW: + cbf_label = "lowcbf" + elif telescope == TELESCOPE.MID: + cbf_label = "midcbf" + else: + raise NotImplementedError( + f"No assign resources for telescope {telescope}" + ) + # Overall assignment schema + return _common.make( + name, + version, + strict, + { + Optional("interface"): str, + "common": get_common_assignres_schema(version, strict), + cbf_label: get_cbf_assignres_schema(version, strict, telescope), + }, + ) + + else: + major_version, _ = _common.split_interface_version(version) + raise ValueError(f"Unknown major schema version: {major_version}") + + else: + raise ValueError(f"Invalid JSON Schema URI {version}") diff --git a/src/ska_telmodel/csp/version.py b/src/ska_telmodel/csp/version.py index b71a560093972d10fdeca8d5c82a674edf9180c8..764e956d292f3218909cd9846e77f95ffd88ebc7 100644 --- a/src/ska_telmodel/csp/version.py +++ b/src/ska_telmodel/csp/version.py @@ -3,6 +3,7 @@ import logging from .._common import split_interface_version CSP_CONFIG_PREFIX = "https://schema.skatelescope.org/ska-csp-configure/" +CSP_ASSIGNRES_PREFIX = "https://schema.skatelescope.org/ska-csp-assignresources/" CSP_CONFIG_VER0 = CSP_CONFIG_PREFIX + "0" # ADR-3 Configuring and Scanning, @@ -14,12 +15,22 @@ CSP_CONFIG_VER0_1 = CSP_CONFIG_PREFIX + "0.1" CSP_CONFIG_VER1 = CSP_CONFIG_PREFIX + "1" CSP_CONFIG_VER1_0 = CSP_CONFIG_PREFIX + "1.0" +CSP_ASSIGNRES_VER0 = CSP_ASSIGNRES_PREFIX + "0" +CSP_ASSIGNRES_VER0_0 = CSP_ASSIGNRES_PREFIX + "0.0" + + # CSP configuration versions, chronologically sorted CSP_CONFIG_VERSIONS = sorted( [CSP_CONFIG_VER0_0, CSP_CONFIG_VER0_1, CSP_CONFIG_VER1_0], key=split_interface_version, ) +# CSP assign resources versions, chronologically sorted +CSP_ASSIGNRES_VERSIONS = sorted( + [CSP_ASSIGNRES_VER0_0], + key=split_interface_version, +) + _LOGGER = logging.getLogger(__name__) @@ -56,3 +67,28 @@ def get_csp_config_version( f"CSP interface URI '%s' not recognised!", csp_interface_version ) return csp_interface_version + + +def validate_csp_assignres_version(csp_interface_version: str): + """Checks whether a csp interface version is valid.""" + return csp_interface_version.startswith(CSP_ASSIGNRES_PREFIX) + + +def get_csp_assignres_version(csp_interface_version: str, csp_assignres: dict = None): + """Determines assign resources interface version + + :param csp_interface_version: External guess at the interface version + :param csp_assignres: Example assign resources data to derive version from + :returns: Canonical URI of interface version + """ + + # Get from example data, if available + if csp_assignres is not None and "interface" in csp_assignres: + csp_interface_version = csp_assignres["interface"] + + # Valid? + if not validate_csp_assignres_version(csp_interface_version): + _LOGGER.warning( + f"CSP interface URI '%s' not recognised!", csp_interface_version + ) + return csp_interface_version diff --git a/src/ska_telmodel/schema.py b/src/ska_telmodel/schema.py index 88384082425f0b5848d23f49d6716fafafa2f49d..3dd24ae3dafafdf3b6287a197ee5f33e1e093d0e 100644 --- a/src/ska_telmodel/schema.py +++ b/src/ska_telmodel/schema.py @@ -15,6 +15,7 @@ _LOGGER = logging.getLogger("ska_telmodel") # the schema for them URI_SCHEMA_MAP = { csp.CSP_CONFIG_PREFIX: csp.get_csp_config_schema, + csp.CSP_ASSIGNRES_PREFIX: csp.get_csp_assignres_schema, # SDP schemas sdp.SDP_ASSIGNRES_PREFIX: sdp.get_sdp_assign_resources_schema, sdp.SDP_CONFIG_PREFIX: sdp.get_sdp_config_schema, @@ -37,6 +38,7 @@ URI_SCHEMA_MAP = { # As above, but for example generation URI_EXAMPLE_MAP = { csp.CSP_CONFIG_PREFIX: csp_examples.get_csp_config_example, + csp.CSP_ASSIGNRES_PREFIX: csp_examples.get_csp_assignres_example, # SDP schemas sdp.SDP_ASSIGNRES_PREFIX: sdp_examples.get_sdp_assignres_example, sdp.SDP_CONFIG_PREFIX: sdp_examples.get_sdp_config_example, diff --git a/tests/test_csp_assign.py b/tests/test_csp_assign.py new file mode 100644 index 0000000000000000000000000000000000000000..71eb5050601b39f563a24c6f8c8f037e0ea5fd04 --- /dev/null +++ b/tests/test_csp_assign.py @@ -0,0 +1,104 @@ +import json +import pytest +import copy +from schema import SchemaError + +from ska_telmodel.schema import validate, example_by_uri +from ska_telmodel._common import TELESCOPE +from ska_telmodel.csp import version, schema, get_csp_assignres_schema +from ska_telmodel.csp.examples import get_csp_assignres_example +from ska_telmodel.csp.version import * + + +def test_newer_major_version(): + + undefined_major_version = version.CSP_ASSIGNRES_PREFIX + "1.0" + with pytest.raises(ValueError, match="Unknown major schema version"): + validate(None, {"interface": undefined_major_version}, 2) + + +def test_unknown_version(caplog): + + invalid_prefix = "http://schema.example.org/invalid" + version.get_csp_assignres_version(None, {"interface": invalid_prefix}) + assert caplog.record_tuples == [ + ( + "ska_telmodel.csp.version", + 30, + f"CSP interface URI '{invalid_prefix}' not recognised!", + ) + ] + + with pytest.raises(ValueError, match=f"Invalid JSON Schema URI {invalid_prefix}"): + # pylint: disable=no-member + schema.get_csp_assignres_schema(invalid_prefix, True) + + +def _test_schema(csp_ver): + csp_assignres_no_pst_or_zoom = get_csp_assignres_example(csp_ver) + csp_assignres_pst = get_csp_assignres_example(csp_ver, "pst") + csp_assignres_zoom = get_csp_assignres_example(csp_ver, "zoom") + + validate(csp_ver, csp_assignres_no_pst_or_zoom, 2) + validate(csp_ver, csp_assignres_pst, 2) + validate(csp_ver, csp_assignres_zoom, 2) + + # invalid combination of parameters. + # ideally we would 'deep merge' the PST and zoom inputs, + # but that is a little tricky to do, so let's hard-code it for now + csp_assignres_invalid = copy.deepcopy(csp_assignres_zoom) + csp_assignres_invalid["lowcbf"]["station_beams"][0]["pst_beams"] = { + "pst_beam_id": 1 + } + with pytest.raises(ValueError, match=r".*Forbidden key encountered: 'pst_beams'.*"): + validate(csp_ver, csp_assignres_invalid, 2) + + # exercise the not implemented exceptions + with pytest.raises(NotImplementedError): + get_csp_assignres_schema(csp_ver, 2, telescope=TELESCOPE.MID) + with pytest.raises(NotImplementedError): + get_csp_assignres_schema(csp_ver, 2, telescope=max(TELESCOPE) + 1) + + +def test_schema(): + for csp_ver in CSP_ASSIGNRES_VERSIONS: + _test_schema(csp_ver) + + +def test_example(): + for csp_ver in CSP_ASSIGNRES_VERSIONS: + example_by_uri(csp_ver) + + +def test_schema_validation_with_invalid_uri(): + csp_assignres_in = get_csp_assignres_example(CSP_ASSIGNRES_VERSIONS[-1]) + csp_assignres_in["interface"] = "https://invalid_uri/2.0" + msg = r"Unknown schema URI kind: https://invalid_uri/2.0!" + with pytest.raises(ValueError, match=msg): + validate(None, csp_assignres_in, 2) + + +def test_schema_validation_with_invalid_version(): + major, _ = split_interface_version(CSP_ASSIGNRES_VERSIONS[-1]) + csp_assignres_in = get_csp_assignres_example(CSP_ASSIGNRES_VERSIONS[-1]) + csp_assignres_in["interface"] = CSP_ASSIGNRES_PREFIX + str(major + 1) + ".0" + with pytest.raises(ValueError, match=r"Unknown major schema version*"): + validate(None, csp_assignres_in, 2) + + +def test_schema_example_with_invalid_parameters(): + + # Check that an invalid URI raises an exception + msg = r"Could not generate example for schema https://invalid_uri/2.0 assignment None!" + with pytest.raises(ValueError, match=msg): + get_csp_assignres_example("https://invalid_uri/2.0") + + # Check that an invalid scan name raises an exception + for uri in CSP_ASSIGNRES_VERSIONS: + msg = f"Could not generate example for schema {uri} assignment FANTASY!" + with pytest.raises(ValueError, match=msg): + get_csp_assignres_example(uri, "FANTASY") + + msg = "Unknown schema URI kind for example: https://invalid_uri/2.0!" + with pytest.raises(ValueError, match=msg): + example_by_uri("https://invalid_uri/2.0")