Skip to content

Commit ac5bc0e

Browse files
authored
Merge pull request #382 from alercebroker/feat/conesearch
feat: implement conesearch functionality with OID and coordinate queries
2 parents 481c2e3 + ffa2dbc commit ac5bc0e

File tree

12 files changed

+777
-21
lines changed

12 files changed

+777
-21
lines changed

multisurveys-apis/poetry.lock

Lines changed: 109 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

multisurveys-apis/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pyyaml = "^6.0.2"
3030
polars = "^1.31.0"
3131
pyarrow = "^20.0.0"
3232
pandas = "^2.3.1"
33+
toolz = "^1.0.0"
34+
astropy = "^7.1.0"
3335

3436
[tool.poetry.group.test.dependencies]
3537
pytest = "^7.4.0"

multisurveys-apis/src/core/idmapper/idmapper.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ def catalog_oid_to_masterid(
4949
The ZTF object ID.
5050
validate: bool
5151
If True, validate the ztf_oid before conversion.
52-
db_cursor: psycopg2.extensions.cursor
53-
Database cursor for LSST catalog. This parameter is required for LSST.
54-
5552
Returns
5653
-------
5754
str
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from typing import Callable, ContextManager, List
2+
from db_plugins.db.sql.models import Object
3+
from numpy import int64
4+
from sqlalchemy import asc, select, text
5+
from sqlalchemy.orm import Session, aliased
6+
7+
8+
def conesearch_coordinates(
9+
session_factory: Callable[..., ContextManager[Session]],
10+
):
11+
def _conesearch(
12+
ra: float,
13+
dec: float,
14+
radius: float,
15+
neighbors: int,
16+
) -> List[Object]:
17+
stmt = _build_statement_coordinates(neighbors)
18+
with session_factory() as session:
19+
result = session.execute(
20+
stmt, {"ra": ra, "dec": dec, "radius": radius}
21+
).all()
22+
return [row[0] for row in result]
23+
24+
return _conesearch
25+
26+
27+
def _build_statement_coordinates(neighbors: int):
28+
return (
29+
select(Object)
30+
.where(text("q3c_radial_query(meanra, meandec, :ra, :dec, :radius)"))
31+
.order_by(asc(text("q3c_dist(meanra, meandec, :ra, :dec)")))
32+
.limit(neighbors)
33+
)
34+
35+
36+
def conesearch_oid(session_factory: Callable[..., ContextManager[Session]]):
37+
def _conesearch(oid: int64, radius: float, neighbors: int) -> List[Object]:
38+
stmt = _build_statement_oid(oid, neighbors)
39+
with session_factory() as session:
40+
result = session.execute(stmt, {"radius": radius}).all()
41+
return [row[0] for row in result]
42+
43+
return _conesearch
44+
45+
46+
def _build_statement_oid(oid: int64, neighbors: int):
47+
# Create aliases for the Object table
48+
center_obj = aliased(Object, name="center")
49+
target_obj = aliased(Object, name="target")
50+
51+
# Build the query using q3c_radial_query function
52+
return (
53+
select(target_obj.oid, target_obj.meanra, target_obj.meandec)
54+
.select_from(center_obj, target_obj)
55+
.where(center_obj.oid == oid.item())
56+
.where(
57+
text(
58+
"q3c_radial_query(target.meanra, target.meandec, center.meanra, center.meandec, :radius)"
59+
)
60+
)
61+
.order_by(
62+
asc(
63+
text(
64+
"q3c_dist(target.meanra, target.meandec, center.meanra, center.meandec)"
65+
)
66+
)
67+
)
68+
.limit(neighbors)
69+
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pydantic import BaseModel
22

33

4-
class AlerceObject(BaseModel):
4+
class ApiObject(BaseModel):
55
objectId: str
66
ra: float
77
dec: float
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
import traceback
2+
from typing import List
23

3-
import numpy as np
44
from fastapi import APIRouter, HTTPException
55

66
from core.config.dependencies import db_dependency
7-
from core.idmapper.idmapper import decode_masterid
7+
from core.idmapper.idmapper import catalog_oid_to_masterid
8+
9+
from ..services.conesearch import conesearch as service
10+
from ..models.object import ApiObject
811

912
router = APIRouter()
1013

1114

12-
@router.get("/conesearch")
15+
@router.get("/conesearch_oid/objects")
1316
def conesearch(
14-
oid: int,
17+
oid: str,
18+
survey: str,
19+
radius: float,
20+
neighbors: int,
1521
db: db_dependency,
16-
):
17-
survey, id = decode_masterid(np.int64(oid))
22+
) -> List[ApiObject]:
23+
try:
24+
id = catalog_oid_to_masterid(survey, oid, True)
25+
except ValueError:
26+
raise HTTPException(status_code=400, detail="Invalid object ID")
27+
1828
try:
19-
return f"{survey} {id}"
29+
return service.conesearch_oid(id, radius, neighbors, db.session)
2030
except Exception:
2131
traceback.print_exc()
2232
raise HTTPException(status_code=500, detail="An error occurred")

multisurveys-apis/src/lightcurve_api/services/conesearch.py

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import List, cast, Callable, ContextManager
2+
from lightcurve_api.services.conesearch.validation import (
3+
validate_coordinates_params,
4+
validate_oid_params,
5+
)
6+
from lightcurve_api.models.object import ApiObject
7+
import core.repository.queries.conesearch as queries
8+
from sqlalchemy.orm import Session
9+
from .parser import parsesapi_objects
10+
from toolz.functoolz import pipe
11+
from numpy import int64
12+
13+
14+
def conesearch_coordinates(
15+
ra: float,
16+
dec: float,
17+
radius: float,
18+
neighbors: int,
19+
session_factory: Callable[..., ContextManager[Session]],
20+
) -> List[ApiObject]:
21+
return cast(
22+
List[ApiObject],
23+
pipe(
24+
(ra, dec, radius, neighbors),
25+
validate_coordinates_params,
26+
queries.conesearch_coordinates(session_factory),
27+
parsesapi_objects,
28+
),
29+
)
30+
31+
32+
def conesearch_oid(
33+
oid: int64,
34+
radius: float,
35+
neighbors: int,
36+
session_factory: Callable[..., ContextManager[Session]],
37+
) -> List[ApiObject]:
38+
return cast(
39+
List[ApiObject],
40+
pipe(
41+
(oid, radius, neighbors),
42+
validate_oid_params,
43+
queries.conesearch_oid(session_factory),
44+
parsesapi_objects,
45+
),
46+
)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import List
2+
from db_plugins.db.sql.models import Object
3+
4+
from lightcurve_api.models.object import ApiObject
5+
from core.idmapper.idmapper import decode_masterid
6+
from numpy import int64
7+
8+
9+
def parse_api_object(sql_object: Object) -> ApiObject:
10+
_, parsedOid = decode_masterid(int64(str(sql_object.oid)))
11+
return ApiObject(
12+
objectId=str(parsedOid),
13+
ra=sql_object.meanra, # type: ignore
14+
dec=sql_object.meandec, # type: ignore
15+
)
16+
17+
18+
def parsesapi_objects(sql_objects: List[Object]) -> List[ApiObject]:
19+
return [parse_api_object(sql_object) for sql_object in sql_objects]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Tuple
2+
from numpy import int64
3+
4+
5+
def validate_coordinates_params(
6+
args: Tuple[float, float, float, int],
7+
) -> Tuple[float, float, float, int]:
8+
ra, dec, radius, neighbors = args
9+
if ra < 0 or ra > 360:
10+
raise ValueError("RA must be greater than 0 and lower than 360")
11+
if dec <= -90 or dec >= 90:
12+
raise ValueError("Dec must be greater than -90 and lower than 90")
13+
if radius <= 0:
14+
raise ValueError("Radius must be greater than 0")
15+
if neighbors <= 0:
16+
raise ValueError("Neighbors must be greater than 0")
17+
return ra, dec, radius, neighbors
18+
19+
20+
def validate_oid_params(
21+
args: Tuple[int64, float, int],
22+
) -> Tuple[int64, float, int]:
23+
oid, radius, neighbors = args
24+
return oid, radius, neighbors

0 commit comments

Comments
 (0)