Source code for data_model.mirror_segmentation

"""Validation and serialization of mirror-segmentation model parameters."""

import math
from functools import lru_cache

import astropy.units as u
from jsonschema.exceptions import ValidationError

from simtools.data_model import schema
from simtools.data_model.json_validation import validate_finite_json_values


@lru_cache
def _segmentation_json_schema(parameter_name, schema_version):
    """Return the JSON schema for a mirror-segmentation parameter."""
    parameter_schema = schema.get_model_parameter_schema(parameter_name, schema_version)
    return next(item["json_schema"] for item in parameter_schema["data"] if item["type"] == "dict")


[docs] @lru_cache def kind_required_fields(parameter_name, schema_version): """Map schema-declared segmentation kinds to their required fields.""" definitions = {} for item in _segmentation_json_schema(parameter_name, schema_version)["items"]["oneOf"]: kinds = item["properties"]["kind"].get("enum", []) for kind in kinds: definitions[kind] = frozenset(item["required"]) return definitions
[docs] def validate_segments( records, parameter_name, schema_version, ): """Validate mirror-segmentation records and return them unchanged. Parameters ---------- records : list of dict Ring, shape, or polygon records. parameter_name : str Mirror-segmentation model parameter whose schema validates the records. schema_version : str Version of the selected parameter schema. Returns ------- list of dict The validated records. Raises ------ ValueError If a record is malformed or contains non-finite geometry. """ try: schema.validate_dict_using_schema( records, json_schema=_segmentation_json_schema(parameter_name, schema_version), ) except ValidationError as exc: raise ValueError(f"Invalid mirror segmentation: {exc.message}") from exc for record in records: validate_finite_json_values(record, path="$[record]") required_fields = kind_required_fields(parameter_name, schema_version)[record["kind"]] if "r_min" in required_fields: _validate_ring(record) elif "vertices" in required_fields: _validate_polygon(record) else: _validate_shape(record) return records
[docs] def quantity_value(record, field, unit, default=None): """Return one segmentation quantity converted to the requested unit. Parameters ---------- record : dict Segmentation record containing an explicit ``value``/``unit`` object. field : str Quantity field name. unit : str Unit to which the value is converted. default : float, optional Value returned when the optional field is absent. Returns ------- float Numeric value in ``unit``. """ quantity = record.get(field) if quantity is None: return default try: return (quantity["value"] * u.Unit(quantity["unit"])).to(unit).value except (KeyError, TypeError, ValueError, u.UnitConversionError) as exc: raise ValueError(f"Invalid unit-bearing segmentation field '{field}'") from exc
[docs] def make_quantity(value, unit): """Return a JSON-compatible explicit quantity object. Parameters ---------- value : float Numeric quantity value. unit : str Unit associated with ``value``. Returns ------- dict Quantity represented by ``value`` and ``unit`` keys. """ return {"value": value, "unit": unit}
def _validate_ring(record): r_min = quantity_value(record, "r_min", "cm") r_max = quantity_value(record, "r_max", "cm") if r_min < 0 or r_max <= 0 or r_max <= r_min: raise ValueError("Ring r_max must be greater than r_min") if quantity_value(record, "dphi", "deg") <= 0: raise ValueError("Ring dphi must be positive") if quantity_value(record, "gap", "cm", 0) < 0: raise ValueError("Ring gap must not be negative") def _validate_shape(record): """Validate the positive size of a shaped mirror facet.""" if quantity_value(record, "diameter", "cm") <= 0: raise ValueError("Shape diameter must be positive") def _validate_polygon(record): vertices = record.get("vertices") vertices = [ {"x": quantity_value(vertex, "x", "cm"), "y": quantity_value(vertex, "y", "cm")} for vertex in vertices ] area = sum( first["x"] * second["y"] - second["x"] * first["y"] for first, second in zip(vertices, vertices[1:] + vertices[:1], strict=False) ) if math.isclose(area, 0, abs_tol=1e-12): raise ValueError("Polygon vertices must enclose a non-zero area")