Coverage for middle_layer/solicit/domain_layer/entities/capability.py: 94.37%
71 statements
« prev ^ index » next coverage.py v7.10.5, created at 2026-04-13 06:13 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2026-04-13 06:13 +0000
1# Copyright 2024 Associated Universities, Inc.
2#
3# This file is part of Telescope Time Allocation Tools (TTAT).
4#
5# TTAT is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# any later version.
9#
10# TTAT is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with TTAT. If not, see <https://www.gnu.org/licenses/>.
17#
18# pyright: reportImportCycles=false
19from collections.abc import Sequence
20from enum import Enum
21from typing import TYPE_CHECKING, TypedDict, TypeVar, override
23import sqlalchemy as sa
24from sqlalchemy import Column, ForeignKey, Integer, Table
25from sqlalchemy.orm import Mapped, mapped_column, relationship
27from common.domain_layer import JSON_OBJECT
28from common.domain_layer.entities.base import Base
29from solicit.domain_layer.entities.array_configuration import ArrayConfiguration
30from solicit.domain_layer.entities.backend import Backend
31from solicit.domain_layer.entities.external_joint_parameter import ExternalJointParameter
32from solicit.domain_layer.entities.frontend import Frontend
33from solicit.domain_layer.entities.station import Station
35if TYPE_CHECKING:
36 from allocate.domain_layer.entities.allocation_version import AllocationVersion
37 from allocate.domain_layer.entities.available_time_model_version import AvailableTimeModelVersion
38 from closeout.domain_layer.entities.prototype_project import PrototypeProject
39 from solicit.domain_layer.entities.solicitation_facility_capability import SolicitationFacilityCapability
42T = TypeVar("T", bound=Base)
45external_joint_required_facilities = Table(
46 "external_joint_required_facilities",
47 Base.metadata,
48 Column(
49 "external_joint_parameter_id",
50 Integer,
51 ForeignKey("external_joint_parameters.external_joint_parameter_id", ondelete="cascade"),
52 nullable=False,
53 ),
54 Column("facility_id", Integer, ForeignKey("facilities.facility_id", ondelete="cascade"), nullable=False),
55 extend_existing=True,
56)
59class Facility(Base):
60 """
61 A resource that the NRAO makes available for Authors to write Proposals to use
62 Relative to a Solicitation, for the moment
63 """
65 __tablename__: str = "facilities"
67 # Unique identifier from backend
68 facility_id: Mapped[int] = mapped_column(primary_key=True)
69 facility_name: Mapped[str] = mapped_column(nullable=False)
70 is_active: Mapped[bool] = mapped_column(nullable=False, default=True)
71 description: Mapped[str] = mapped_column(nullable=True)
72 requested_amount_unit: Mapped[str] = mapped_column()
74 allocation_versions: Mapped[list["AllocationVersion"]] = relationship(
75 "AllocationVersion", back_populates="facility", cascade="all, delete"
76 )
77 allocation_requests: Mapped[list["AllocationVersion"]] = relationship(
78 "AllocationRequest", back_populates="facility", cascade="all, delete"
79 )
80 available_time_model_versions: Mapped[list["AvailableTimeModelVersion"]] = relationship(
81 "AvailableTimeModelVersion", back_populates="facility", cascade="all, delete"
82 )
83 prototype_projects: Mapped[list["PrototypeProject"]] = relationship(
84 "PrototypeProject", back_populates="facility", cascade="all, delete", passive_deletes=True
85 )
86 solicitation_facility_capabilities: Mapped[list["SolicitationFacilityCapability"]] = relationship(
87 "SolicitationFacilityCapability", back_populates="facility", cascade="all, delete"
88 )
89 frontends: Mapped[list[Frontend]] = relationship(
90 back_populates="facility", cascade="all, delete-orphan", passive_deletes=True
91 )
92 backends: Mapped[list[Backend]] = relationship(
93 back_populates="facility", cascade="all, delete-orphan", passive_deletes=True
94 )
95 stations: Mapped[list[Station]] = relationship(
96 back_populates="facility", cascade="all, delete-orphan", passive_deletes=True
97 )
98 array_configurations: Mapped[list[ArrayConfiguration]] = relationship(
99 back_populates="facility", cascade="all, delete-orphan", passive_deletes=True
100 )
101 external_joint_parameters: Mapped[list[ExternalJointParameter]] = relationship(
102 back_populates="required_facilities",
103 secondary=external_joint_required_facilities,
104 )
106 def __init__(self, name: str) -> None:
107 super().__init__(facility_name=name)
109 @override
110 def __repr__(self) -> str:
111 return f"<Facility: {self.facility_name}>"
113 @override
114 def __json__(self) -> JSON_OBJECT:
115 facility_json: JSON_OBJECT = super().__json__()
117 # map attributes to JSON keys
118 attribute_map: dict[str, Sequence[Base]] = {
119 "frontends": self.frontends,
120 "backends": self.backends,
121 "arrayConfigurations": self.array_configurations,
122 "stations": self.stations,
123 }
124 for js_attr, python_attr in attribute_map.items():
125 if python_attr:
126 facility_json[js_attr] = [f.__json__() for f in python_attr]
127 return facility_json
129 @override
130 def __eq__(self, other: object) -> bool:
131 return (
132 isinstance(other, self.__class__)
133 and self.facility_id == other.facility_id
134 and self.facility_name == other.facility_name
135 and self.is_active == other.is_active
136 )
138 @override
139 def __hash__(self):
140 return hash((self.facility_id, self.facility_name, self.is_active))
143class RequestSpecificationType(Enum):
144 OBSERVATION_SPECIFICATION = "Observation Specification"
145 RESOURCE_SPECIFICATION = "Resource Specification"
146 DATA_PROCESSING_SPECIFICATION = "Data Processing Specification"
149class Capability(Base):
150 __tablename__: str = "capabilities"
151 """
152 A template for a SolicitationCapability.
153 The different ways a Facility may be operated and the resources available.
155 Solicitations cannot share a capability, and capabilities must be copied if applied to another Solicitation.
156 This is so changes (e.g. CapabilityParameterSpecifications) can be adjusted independently for each capability.
157 """
159 capability_id: Mapped[int] = mapped_column(primary_key=True)
160 capability_name: Mapped[str] = mapped_column()
161 is_active: Mapped[bool] = mapped_column()
162 description: Mapped[str] = mapped_column(nullable=True)
163 request_specification_type: Mapped[RequestSpecificationType] = mapped_column(
164 sa.Enum(RequestSpecificationType, name="request_specification_type"),
165 nullable=False,
166 default=RequestSpecificationType.OBSERVATION_SPECIFICATION,
167 )
168 solicitation_facility_capabilities: Mapped[list["SolicitationFacilityCapability"]] = relationship(
169 "SolicitationFacilityCapability", back_populates="capability", cascade="all, delete"
170 )
172 @property
173 def is_external_joint(self) -> bool:
174 return self.capability_name == "External Joint"
176 def __init__(
177 self,
178 name: str,
179 is_active: bool = True,
180 request_specification_type: RequestSpecificationType = RequestSpecificationType.OBSERVATION_SPECIFICATION,
181 cps_list: list["SolicitationFacilityCapability"] | None = None,
182 ):
183 super().__init__(
184 capability_name=name,
185 is_active=is_active,
186 request_specification_type=request_specification_type,
187 )
189 @override
190 def __repr__(self):
191 return f"<Capability: {self.capability_name} at {hex(id(self))}>"
194class RequiredCPS(TypedDict):
195 name: str
196 children: list["RequiredCPS"]
199REQUIRED_CPSS_VLA_CONTINUUM: list[RequiredCPS] = [
200 {
201 "name": "Field Sources",
202 "children": [
203 {"name": "Right Ascension", "children": []},
204 {"name": "Declination", "children": []},
205 {"name": "Name", "children": []},
206 ],
207 },
208 {
209 "name": "Spectral Specifications",
210 "children": [
211 {"name": "Center Frequency", "children": []},
212 {"name": "Bandwidth", "children": []},
213 {"name": "Name", "children": []},
214 ],
215 },
216 {"name": "RMS Sensitivity", "children": []},
217]
218REQUIRED_CPSS_GBT_SPECTRAL_LINE: list[RequiredCPS] = [
219 {
220 "name": "Field Sources",
221 "children": [
222 {"name": "Right Ascension", "children": []},
223 {"name": "Declination", "children": []},
224 {"name": "Name", "children": []},
225 ],
226 },
227 {
228 "name": "Spectral Specifications",
229 "children": [
230 {"name": "Center Frequency", "children": []},
231 {"name": "Bandwidth", "children": []},
232 {"name": "Name", "children": []},
233 ],
234 },
235 {"name": "RMS Sensitivity", "children": []},
236]