Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ SOCAT

The Source CATalog.

Contains information about sources that are being monitored by an obseratory,
Contains information about sources that are being monitored by an observatory,
or that are being used to crossmatch detected sources with.


Expand All @@ -27,15 +27,15 @@ Once created, you can add sources via the following example ingest scripts:

`socat-jpl-parquet` was written to read in a parquet file containing JPL Horizons ephemerides for solar system objects.

These scripts store either FixedRegisteredSources, or SolarSystemObjects.
These scripts store either RegisteredFixedSources, or SolarSystemObjects.


USING SOCAT
=====

Once sources are ingested into your db, you can use the socat to query sources within a box on the sky within some time range.

Make sure you environment variables point to the correct db, as above.
Make sure your environment variables point to the correct db, as above.

```
from socat.client.settings import SOCatClientSettings
Expand Down
2 changes: 1 addition & 1 deletion socat/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool

from alembic import context
from socat.settings import settings

Check failure on line 6 in socat/alembic/env.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/env.py:1:1: I001 Import block is un-sorted or un-formatted

Check failure on line 6 in socat/alembic/env.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/env.py:1:1: I001 Import block is un-sorted or un-formatted

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
Expand Down
1 change: 1 addition & 0 deletions socat/alembic/versions/35a6a33e0a34_initial_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

Check failure on line 13 in socat/alembic/versions/35a6a33e0a34_initial_database.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/versions/35a6a33e0a34_initial_database.py:9:1: I001 Import block is un-sorted or un-formatted

Check failure on line 13 in socat/alembic/versions/35a6a33e0a34_initial_database.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/versions/35a6a33e0a34_initial_database.py:9:1: I001 Import block is un-sorted or un-formatted

# revision identifiers, used by Alembic.
revision: str = "35a6a33e0a34"
Expand Down
39 changes: 39 additions & 0 deletions socat/alembic/versions/a1b2c3d4e5f6_add_monitored_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Add monitored field to fixed_sources and solarsystem_objects

Revision ID: a1b2c3d4e5f6
Revises: 35a6a33e0a34
Create Date: 2026-06-01 00:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

Check failure on line 13 in socat/alembic/versions/a1b2c3d4e5f6_add_monitored_field.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/versions/a1b2c3d4e5f6_add_monitored_field.py:9:1: I001 Import block is un-sorted or un-formatted

Check failure on line 13 in socat/alembic/versions/a1b2c3d4e5f6_add_monitored_field.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (I001)

socat/alembic/versions/a1b2c3d4e5f6_add_monitored_field.py:9:1: I001 Import block is un-sorted or un-formatted

# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f6"
down_revision: str | None = "35a6a33e0a34"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
op.add_column(
"fixed_sources",
sa.Column("monitored", sa.Boolean, nullable=False, server_default=sa.false()),
)
op.execute("UPDATE fixed_sources SET monitored = false WHERE monitored IS NULL")
op.add_column(
"solarsystem_objects",
sa.Column("monitored", sa.Boolean, nullable=False, server_default=sa.false()),
)
op.execute(
"UPDATE solarsystem_objects SET monitored = false WHERE monitored IS NULL"
)
Comment thread
axf295 marked this conversation as resolved.


def downgrade() -> None:
op.drop_column("fixed_sources", "monitored")
op.drop_column("solarsystem_objects", "monitored")
17 changes: 13 additions & 4 deletions socat/client/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
class ClientBase(ABC):
@abstractmethod
def create_source(
self, *, position: ICRS, name: str | None = None, flux: Quantity | None = None
self,
*,
position: ICRS,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool = False,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So my main thought on this is do we want a specific monitored flag, which is an attribute of the source, or do we rather want a flags attribute, which is say list[str], where the code can interact with this list and check for relevant flags like monitored? The advantage of the first approach is simplicity; the allowable flags are obvious by looking at the attributes of sources, no need for flag sanitation, etc. The advantage of the second is extensibility. If we want to add a new flag, we don't need to do a whole database migration, we don't need to add new_flag as an attribute everywhere, and we just need to write/modify the portions of the code which interact with that flag. This seems like it's a known problem/set of trade offs in DB design, maybe @JBorrow has ideas.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea I could go either way. forced_photometry is a core functionality of this so maybe that exists outside of 'flags' but 'flags' could contain 'pointing source' or 'extended' , etc that we want to use . and people could do custom things if they wanted.

) -> RegisteredFixedSource:
"""
Create a new source in the catlaog.
Expand Down Expand Up @@ -59,10 +64,11 @@ def get_source(self, *, source_id: int) -> RegisteredFixedSource | None:

@abstractmethod
def get_forced_photometry_sources(
self, *, minimum_flux: Quantity
self, *, minimum_flux: Quantity | None = None
) -> list[RegisteredFixedSource]:
"""
Get all sources that are used for forced photometry based on a minimum flux.
Get all sources that are monitored for forced photometry, optionally
filtered to those above a minimum flux.
"""
return [] # pragma: no cover

Expand All @@ -74,6 +80,7 @@ def update_source(
position: ICRS | None = None,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool | None = None,
) -> RegisteredFixedSource | None:
"""
Update a source. If the source is updated, return its new value. Else, return None.
Expand Down Expand Up @@ -219,7 +226,9 @@ def delete_ephem(self, *, ephem_id: int) -> None:

class SolarSystemClientBase(ABC):
@abstractmethod
def create_sso(self, *, name: str, MPC_id: int | None) -> SolarSystemObject:
def create_sso(
self, *, name: str, MPC_id: int | None, monitored: bool = False
) -> SolarSystemObject:
"""
Create a new solar system source in the catalog.
"""
Expand Down
67 changes: 52 additions & 15 deletions socat/client/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ def ephem(self) -> EphemClientBase:
return self._ephem

def create_source(
self, *, position: ICRS, name: str | None = None, flux: Quantity | None = None
self,
*,
position: ICRS,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool = False,
) -> RegisteredFixedSource:
if flux is not None:
flux = flux.to_value("mJy")
Expand All @@ -93,6 +98,7 @@ def create_source(
dec_deg=position.dec.to_value("deg"),
name=name,
flux_mJy=flux,
monitored=monitored,
)
with self._get_session() as session:
session.add(source)
Expand Down Expand Up @@ -129,7 +135,7 @@ def get_source(self, *, source_id: int) -> RegisteredFixedSource | None:
return source.to_model()

def get_forced_photometry_sources(
self, *, minimum_flux: Quantity
self, *, minimum_flux: Quantity | None = None
) -> list[RegisteredFixedSource]:
with self._get_session() as session:
sources = session.execute(
Expand All @@ -144,6 +150,7 @@ def update_source(
position: ICRS | None = None,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool | None = None,
) -> RegisteredFixedSource | None:
with self._get_session() as session:
session.execute(
Expand All @@ -152,6 +159,7 @@ def update_source(
position=position,
name=name,
flux=flux,
monitored=monitored,
)
)
source = session.get(RegisteredFixedSourceTable, source_id)
Expand Down Expand Up @@ -404,8 +412,10 @@ def __init__(
session_factory=session_factory,
)

def create_sso(self, *, name: str, MPC_id: int | None) -> SolarSystemObject:
source = SolarSystemObjectTable(name=name, MPC_id=MPC_id)
def create_sso(
self, *, name: str, MPC_id: int | None, monitored: bool = False
) -> SolarSystemObject:
source = SolarSystemObjectTable(name=name, MPC_id=MPC_id, monitored=monitored)

with self._get_session() as session:
session.add(source)
Expand Down Expand Up @@ -574,30 +584,53 @@ def init_interp(self, *, ephem_cat: EphemClient) -> None:
if type(self.source) is RegisteredFixedSource:
self.ra_unit = self.source.position.ra.unit
self.dec_unit = self.source.position.dec.unit
self.flux_unit = self.source.flux.unit
self.do_flux = self.source.flux is not None
self.flux_unit = self.source.flux.unit if self.do_flux else None
self.interp = lambda _: (
self.source.position.ra.value,
self.source.position.dec.value,
self.source.flux.value,
(
self.source.position.ra.value,
self.source.position.dec.value,
self.source.flux.value,
)
if self.do_flux
else (
self.source.position.ra.value,
self.source.position.dec.value,
)
)

elif type(self.source) is SolarSystemObject:
ephem_points = ephem_cat.get_ephem_points(
sso_id=self.source.sso_id, t_min=self.t_min, t_max=self.t_max
)
x = np.zeros(len(ephem_points))
y = np.zeros((len(ephem_points), 3))

self.do_flux = True
for ephem in ephem_points:
if ephem.flux is None:
self.do_flux = False
break

if self.do_flux:
y = np.zeros((len(ephem_points), 3))
else:
y = np.zeros((len(ephem_points), 2))

for i, ephem in enumerate(ephem_points):
x[i] = ephem.time.unix
y[i] = (
ephem.position.ra.value,
ephem.position.dec.value,
ephem.flux.value,
(
ephem.position.ra.value,
ephem.position.dec.value,
ephem.flux.value,
)
if self.do_flux
else (ephem.position.ra.value, ephem.position.dec.value)
)

self.ra_unit = ephem.position.ra.unit
self.dec_unit = ephem.position.dec.unit
self.flux_unit = ephem.flux.unit
self.flux_unit = ephem.flux.unit if self.do_flux else None
self.interp = make_interp_spline(x, y, k=1)

def at_time(self, *, t: Time) -> tuple[ICRS, Quantity]:
Expand Down Expand Up @@ -631,9 +664,13 @@ def at_time(self, *, t: Time) -> tuple[ICRS, Quantity]:
if t < self.t_min or t > self.t_max:
raise ValueError("Time out of range for source generator")

ra_deg, dec_deg, flux_mJy = self.interp(t.unix)
if self.do_flux:
ra_deg, dec_deg, flux_mJy = self.interp(t.unix)
flux = flux_mJy * self.flux_unit
else:
ra_deg, dec_deg = self.interp(t.unix)
flux = None
Comment thread
axf295 marked this conversation as resolved.

position = ICRS(ra_deg * self.ra_unit, dec_deg * self.dec_unit)
flux = flux_mJy * self.flux_unit

return (position, flux)
47 changes: 34 additions & 13 deletions socat/client/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ def ephem(self) -> EphemClientBase:
return self._ephem

def create_source(
self, *, position: ICRS, name: str | None = None, flux: Quantity | None = None
self,
*,
position: ICRS,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool = False,
) -> RegisteredFixedSource:
"""
Create a new source and add it to the catalog.
Expand All @@ -95,6 +100,8 @@ def create_source(
Flux of source.
name : str | None, Default: None
Name of source
monitored : bool, Default: False
Whether this source is monitored by forced_photometry

Returns
-------
Expand All @@ -108,6 +115,7 @@ def create_source(
position=position,
flux=flux,
name=name,
monitored=monitored,
)
self.catalog[self.n] = source
self.n += 1
Expand Down Expand Up @@ -228,25 +236,28 @@ def get_source(self, *, source_id: int) -> RegisteredFixedSource | None:
return self.catalog.get(source_id, None)

def get_forced_photometry_sources(
self, *, minimum_flux: Quantity
self, *, minimum_flux: Quantity | None = None
) -> list[RegisteredFixedSource]:
"""
Get all sources that are used for forced photometry based on a minimum flux.
Get all monitored sources, optionally filtered to those above a minimum flux.

Parameters
----------
minimum_flux : Quantity
Minimum flux for source to be included
minimum_flux : Quantity | None
If provided, additionally filter to sources with flux >= minimum_flux.

Returns
-------
filter : iterable[RegisteredFixedSource]
List of sources with flux greater than minimum_flux
list[RegisteredFixedSource]
List of monitored sources, filtered by flux if minimum_flux is given.
"""
return filter(
lambda x: x.flux is not None and x.flux >= minimum_flux,
self.catalog.values(),
)
sources = filter(lambda x: x.monitored, self.catalog.values())
if minimum_flux is not None:
sources = filter(
lambda x: x.flux is not None and x.flux >= minimum_flux,
sources,
)
return list(sources)

def update_source(
self,
Expand All @@ -255,6 +266,7 @@ def update_source(
position: ICRS | None = None,
name: str | None = None,
flux: Quantity | None = None,
monitored: bool | None = None,
) -> RegisteredFixedSource | None:
"""
Update a source by id
Expand All @@ -267,6 +279,8 @@ def update_source(
Name of source
flux : Quantity | None, Default: None
Flux of source
monitored : bool | None, Default: None
Whether this source is monitored by forced_photometry

Returns
-------
Expand All @@ -283,6 +297,7 @@ def update_source(
position=current.position if position is None else position,
name=current.name if name is None else name,
flux=current.flux if flux is None else flux,
monitored=current.monitored if monitored is None else monitored,
)

self.catalog[source_id] = new
Expand Down Expand Up @@ -687,7 +702,9 @@ def __init__(self):
self.catalog = {}
self.n = 0

def create_sso(self, *, name: str, MPC_id: int | None) -> SolarSystemObject:
def create_sso(
self, *, name: str, MPC_id: int | None, monitored: bool = False
) -> SolarSystemObject:
"""
Create a new solar system source.

Expand All @@ -697,13 +714,17 @@ def create_sso(self, *, name: str, MPC_id: int | None) -> SolarSystemObject:
Name of source
MPC_id : int
Minor Planet Center ID of source
monitored : bool, Default: False
Whether this source is monitored by forced_photometry

Returns
-------
solar_source : SolarSystemObject
Solar system source that was added.
"""
solar_source = SolarSystemObject(sso_id=self.n, name=name, MPC_id=MPC_id)
solar_source = SolarSystemObject(
sso_id=self.n, name=name, MPC_id=MPC_id, monitored=monitored
)
self.catalog[self.n] = solar_source
self.n += 1

Expand Down
Loading
Loading