Coverage for python/lsst/dax/apdb/apdb.py: 94%

51 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-08 01:12 -0800

1# This file is part of dax_apdb. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["Apdb", "ApdbConfig"] 

25 

26from abc import ABC, abstractmethod 

27from collections.abc import Iterable, Mapping 

28from typing import TYPE_CHECKING 

29 

30import astropy.time 

31import pandas 

32 

33from lsst.resources import ResourcePathExpression 

34from lsst.sphgeom import Region 

35 

36from .apdbSchema import ApdbTables 

37from .config import ApdbConfig 

38from .factory import make_apdb 

39from .schema_model import Table 

40 

41if TYPE_CHECKING: 

42 from .apdbAdmin import ApdbAdmin 

43 from .apdbMetadata import ApdbMetadata 

44 

45 

46class Apdb(ABC): 

47 """Abstract interface for APDB.""" 

48 

49 @classmethod 

50 def from_config(cls, config: ApdbConfig) -> Apdb: 

51 """Create Ppdb instance from configuration object. 

52 

53 Parameters 

54 ---------- 

55 config : `ApdbConfig` 

56 Configuration object, type of this object determines type of the 

57 Apdb implementation. 

58 

59 Returns 

60 ------- 

61 apdb : `apdb` 

62 Instance of `Apdb` class. 

63 """ 

64 return make_apdb(config) 

65 

66 @classmethod 

67 def from_uri(cls, uri: ResourcePathExpression) -> Apdb: 

68 """Make Apdb instance from a serialized configuration. 

69 

70 Parameters 

71 ---------- 

72 uri : `~lsst.resources.ResourcePathExpression` 

73 URI or local file path pointing to a file with serialized 

74 configuration, or a string with a "label:" prefix. In the latter 

75 case, the configuration will be looked up from an APDB index file 

76 using the label name that follows the prefix. The APDB index file's 

77 location is determined by the ``DAX_APDB_INDEX_URI`` environment 

78 variable. 

79 

80 Returns 

81 ------- 

82 apdb : `apdb` 

83 Instance of `Apdb` class, the type of the returned instance is 

84 determined by configuration. 

85 """ 

86 config = ApdbConfig.from_uri(uri) 

87 return make_apdb(config) 

88 

89 @abstractmethod 

90 def getConfig(self) -> ApdbConfig: 

91 """Return APDB configuration for this instance, including any updates 

92 that may be read from database. 

93 

94 Returns 

95 ------- 

96 config : `ApdbConfig` 

97 APDB configuration. 

98 """ 

99 raise NotImplementedError() 

100 

101 @abstractmethod 

102 def tableDef(self, table: ApdbTables) -> Table | None: 

103 """Return table schema definition for a given table. 

104 

105 Parameters 

106 ---------- 

107 table : `ApdbTables` 

108 One of the known APDB tables. 

109 

110 Returns 

111 ------- 

112 tableSchema : `.schema_model.Table` or `None` 

113 Table schema description, `None` is returned if table is not 

114 defined by this implementation. 

115 """ 

116 raise NotImplementedError() 

117 

118 @abstractmethod 

119 def getDiaObjects(self, region: Region) -> pandas.DataFrame: 

120 """Return catalog of DiaObject instances from a given region. 

121 

122 This method returns only the last version of each DiaObject, 

123 and may return only the subset of the DiaObject columns needed 

124 for AP association. Some 

125 records in a returned catalog may be outside the specified region, it 

126 is up to a client to ignore those records or cleanup the catalog before 

127 futher use. 

128 

129 Parameters 

130 ---------- 

131 region : `lsst.sphgeom.Region` 

132 Region to search for DIAObjects. 

133 

134 Returns 

135 ------- 

136 catalog : `pandas.DataFrame` 

137 Catalog containing DiaObject records for a region that may be a 

138 superset of the specified region. 

139 """ 

140 raise NotImplementedError() 

141 

142 @abstractmethod 

143 def getDiaSources( 

144 self, 

145 region: Region, 

146 object_ids: Iterable[int] | None, 

147 visit_time: astropy.time.Time, 

148 start_time: astropy.time.Time | None = None, 

149 ) -> pandas.DataFrame | None: 

150 """Return catalog of DiaSource instances from a given region. 

151 

152 Parameters 

153 ---------- 

154 region : `lsst.sphgeom.Region` 

155 Region to search for DIASources. 

156 object_ids : iterable [ `int` ], optional 

157 List of DiaObject IDs to further constrain the set of returned 

158 sources. If `None` then returned sources are not constrained. If 

159 list is empty then empty catalog is returned with a correct 

160 schema. 

161 visit_time : `astropy.time.Time` 

162 Time of the current visit. If APDB contains records later than this 

163 time they may also be returned. 

164 start_time : `astropy.time.Time`, optional 

165 Lower bound of time window for the query. If not specified then 

166 it is calculated using ``visit_time`` and 

167 ``read_forced_sources_months`` configuration parameter. 

168 

169 Returns 

170 ------- 

171 catalog : `pandas.DataFrame`, or `None` 

172 Catalog containing DiaSource records. `None` is returned if 

173 ``start_time`` is not specified and ``read_sources_months`` 

174 configuration parameter is set to 0. 

175 

176 Notes 

177 ----- 

178 This method returns DiaSource catalog for a region with additional 

179 filtering based on DiaObject IDs. Only a subset of DiaSource history 

180 is returned limited by ``read_sources_months`` config parameter, w.r.t. 

181 ``visit_time``. If ``object_ids`` is empty then an empty catalog is 

182 always returned with the correct schema (columns/types). If 

183 ``object_ids`` is `None` then no filtering is performed and some of the 

184 returned records may be outside the specified region. 

185 """ 

186 raise NotImplementedError() 

187 

188 @abstractmethod 

189 def getDiaForcedSources( 

190 self, 

191 region: Region, 

192 object_ids: Iterable[int] | None, 

193 visit_time: astropy.time.Time, 

194 start_time: astropy.time.Time | None = None, 

195 ) -> pandas.DataFrame | None: 

196 """Return catalog of DiaForcedSource instances from a given region. 

197 

198 Parameters 

199 ---------- 

200 region : `lsst.sphgeom.Region` 

201 Region to search for DIASources. 

202 object_ids : iterable [ `int` ], optional 

203 List of DiaObject IDs to further constrain the set of returned 

204 sources. If list is empty then empty catalog is returned with a 

205 correct schema. If `None` then returned sources are not 

206 constrained. 

207 visit_time : `astropy.time.Time` 

208 Time of the current visit. If APDB contains records later than this 

209 time they may also be returned. 

210 start_time : `astropy.time.Time`, optional 

211 Lower bound of time window for the query. If not specified then 

212 it is calculated using ``visit_time`` and 

213 ``read_forced_sources_months`` configuration parameter. 

214 

215 Returns 

216 ------- 

217 catalog : `pandas.DataFrame`, or `None` 

218 Catalog containing DiaForcedSource records. `None` is returned if 

219 ``start_time`` is not specified and ``read_forced_sources_months`` 

220 configuration parameter is set to 0. 

221 

222 Raises 

223 ------ 

224 NotImplementedError 

225 May be raised by some implementations if ``object_ids`` is `None`. 

226 

227 Notes 

228 ----- 

229 This method returns DiaForcedSource catalog for a region with 

230 additional filtering based on DiaObject IDs. Only a subset of DiaSource 

231 history is returned limited by ``read_forced_sources_months`` config 

232 parameter, w.r.t. ``visit_time``. If ``object_ids`` is empty then an 

233 empty catalog is always returned with the correct schema 

234 (columns/types). If ``object_ids`` is `None` then no filtering is 

235 performed and some of the returned records may be outside the specified 

236 region. 

237 """ 

238 raise NotImplementedError() 

239 

240 @abstractmethod 

241 def containsVisitDetector( 

242 self, 

243 visit: int, 

244 detector: int, 

245 region: Region, 

246 visit_time: astropy.time.Time, 

247 ) -> bool: 

248 """Test whether any sources for a given visit-detector are present in 

249 the APDB. 

250 

251 Parameters 

252 ---------- 

253 visit, detector : `int` 

254 The ID of the visit-detector to search for. 

255 region : `lsst.sphgeom.Region` 

256 Region corresponding to the visit/detector combination. 

257 visit_time : `astropy.time.Time` 

258 Visit time (as opposed to visit processing time). This can be any 

259 timestamp in the visit timespan, e.g. its begin or end time. 

260 

261 Returns 

262 ------- 

263 present : `bool` 

264 `True` if at least one DiaSource or DiaForcedSource record 

265 may exist for the specified observation, `False` otherwise. 

266 """ 

267 raise NotImplementedError() 

268 

269 @abstractmethod 

270 def getSSObjects(self) -> pandas.DataFrame: 

271 """Return catalog of SSObject instances. 

272 

273 Returns 

274 ------- 

275 catalog : `pandas.DataFrame` 

276 Catalog containing SSObject records, all existing records are 

277 returned. 

278 """ 

279 raise NotImplementedError() 

280 

281 @abstractmethod 

282 def store( 

283 self, 

284 visit_time: astropy.time.Time, 

285 objects: pandas.DataFrame, 

286 sources: pandas.DataFrame | None = None, 

287 forced_sources: pandas.DataFrame | None = None, 

288 ) -> None: 

289 """Store all three types of catalogs in the database. 

290 

291 Parameters 

292 ---------- 

293 visit_time : `astropy.time.Time` 

294 Time of the visit. 

295 objects : `pandas.DataFrame` 

296 Catalog with DiaObject records. 

297 sources : `pandas.DataFrame`, optional 

298 Catalog with DiaSource records. 

299 forced_sources : `pandas.DataFrame`, optional 

300 Catalog with DiaForcedSource records. 

301 

302 Notes 

303 ----- 

304 This methods takes DataFrame catalogs, their schema must be 

305 compatible with the schema of APDB table: 

306 

307 - column names must correspond to database table columns 

308 - types and units of the columns must match database definitions, 

309 no unit conversion is performed presently 

310 - columns that have default values in database schema can be 

311 omitted from catalog 

312 - this method knows how to fill interval-related columns of DiaObject 

313 (validityStart, validityEnd) they do not need to appear in a 

314 catalog 

315 - source catalogs have ``diaObjectId`` column associating sources 

316 with objects 

317 

318 This operation need not be atomic, but DiaSources and DiaForcedSources 

319 will not be stored until all DiaObjects are stored. 

320 """ 

321 raise NotImplementedError() 

322 

323 @abstractmethod 

324 def storeSSObjects(self, objects: pandas.DataFrame) -> None: 

325 """Store or update SSObject catalog. 

326 

327 Parameters 

328 ---------- 

329 objects : `pandas.DataFrame` 

330 Catalog with SSObject records. 

331 

332 Notes 

333 ----- 

334 If SSObjects with matching IDs already exist in the database, their 

335 records will be updated with the information from provided records. 

336 """ 

337 raise NotImplementedError() 

338 

339 @abstractmethod 

340 def reassignDiaSources(self, idMap: Mapping[int, int]) -> None: 

341 """Associate DiaSources with SSObjects, dis-associating them 

342 from DiaObjects. 

343 

344 Parameters 

345 ---------- 

346 idMap : `Mapping` 

347 Maps DiaSource IDs to their new SSObject IDs. 

348 

349 Raises 

350 ------ 

351 ValueError 

352 Raised if DiaSource ID does not exist in the database. 

353 """ 

354 raise NotImplementedError() 

355 

356 @abstractmethod 

357 def dailyJob(self) -> None: 

358 """Implement daily activities like cleanup/vacuum. 

359 

360 What should be done during daily activities is determined by 

361 specific implementation. 

362 """ 

363 raise NotImplementedError() 

364 

365 @abstractmethod 

366 def countUnassociatedObjects(self) -> int: 

367 """Return the number of DiaObjects that have only one DiaSource 

368 associated with them. 

369 

370 Used as part of ap_verify metrics. 

371 

372 Returns 

373 ------- 

374 count : `int` 

375 Number of DiaObjects with exactly one associated DiaSource. 

376 

377 Notes 

378 ----- 

379 This method can be very inefficient or slow in some implementations. 

380 """ 

381 raise NotImplementedError() 

382 

383 @property 

384 @abstractmethod 

385 def metadata(self) -> ApdbMetadata: 

386 """Object controlling access to APDB metadata (`ApdbMetadata`).""" 

387 raise NotImplementedError() 

388 

389 @property 

390 @abstractmethod 

391 def admin(self) -> ApdbAdmin: 

392 """Object providing adminitrative interface for APDB (`ApdbAdmin`).""" 

393 raise NotImplementedError()