Skip to content

aimbat.models

ORM classes mapping to AIMBAT database tables.

Each class in this module corresponds to a table in the SQLite project database and is built on SQLModel, which combines SQLAlchemy (for database access) with Pydantic (for validation).

The main classes and their relationships are:

  • AimbatEvent — a seismic event.
  • AimbatStation — a seismic recording station.
  • AimbatSeismogram — links an AimbatEvent to an AimbatStation and holds the timing metadata (begin_time, delta, t0). Waveform data are accessed via the associated AimbatDataSource.
  • AimbatDataSource — identifies the logical source of the waveform data for a seismogram and its type (e.g. SAC).
  • AimbatEventParameters — processing parameters shared across all seismograms of an event (window bounds, bandpass filter settings, MCCC settings, etc.).
  • AimbatSeismogramParameters — per-seismogram processing parameters (flip, select, working pick t1).
  • AimbatSnapshot — captures a point-in-time copy of event and seismogram parameters via AimbatEventParametersSnapshot and AimbatSeismogramParametersSnapshot, enabling rollback and comparison.
  • AimbatEventQuality / AimbatSeismogramQuality — live quality metrics updated during processing; AimbatSeismogramQuality stores the ICCS cross-correlation coefficient iccs_cc and MCCC per-seismogram metrics; AimbatEventQuality stores the MCCC global RMSE.
  • AimbatEventQualitySnapshot / AimbatSeismogramQualitySnapshot — point-in-time copies of quality metrics captured alongside parameter snapshots.

Type Aliases:

Name Description
AimbatTypes

Union of all AIMBAT models that exist in the database.

Classes:

Name Description
AimbatDataSource

Logical source identifier and format of the waveform data for a seismogram.

AimbatEvent

Seismic event with origin time, location, and depth.

AimbatEventParameters

Processing parameters common to all seismograms of a particular event.

AimbatEventParametersBase

Base class defining event-level processing parameters for AIMBAT.

AimbatEventParametersSnapshot

Snapshot of processing parameters for a particular event.

AimbatEventQuality

Live quality metrics for a seismic event.

AimbatEventQualityBase

Base class defining event-level quality metrics.

AimbatEventQualitySnapshot

Snapshot of quality metrics for a seismic event.

AimbatEventRead

Read model for AimbatEvent including computed counts.

AimbatNote

Free-text Markdown note attached to an event, station, seismogram, or snapshot.

AimbatSeismogram

Seismogram metadata linking a recording station to an event.

AimbatSeismogramParameters

Processing parameters for a single seismogram.

AimbatSeismogramParametersBase

Base class defining seismogram-level processing parameters for AIMBAT.

AimbatSeismogramParametersSnapshot

Snapshot of processing parameters for a single seismogram.

AimbatSeismogramQuality

Live quality metrics for a single seismogram.

AimbatSeismogramQualityBase

Base class defining seismogram-level quality metrics.

AimbatSeismogramQualitySnapshot

Snapshot of quality metrics for a single seismogram.

AimbatSeismogramRead

Read model for AimbatSeismogram including parameters.

AimbatSnapshot

Container for a point-in-time snapshot of event and seismogram parameters.

AimbatSnapshotRead

Read model for AimbatSnapshot with a seismogram count.

AimbatStation

Recording station with network, location, and channel metadata.

AimbatStationRead

Read model for AimbatStation including aggregated quality statistics.

SeismogramQualityStats

Aggregated seismogram quality statistics for an event or station.

SnapshotResults

Full results export for a snapshot.

SnapshotSeismogramResult

Per-seismogram result record from a snapshot.

Functions:

Name Description
undefer_counts

Loader options that fetch model_class's count columns in the main query.

AimbatDataSource

Bases: SQLModel

Logical source identifier and format of the waveform data for a seismogram.

Parameters:

Name Type Description Default
id UUID

Unique data source ID.

UUID('b9c5d821-9f38-4667-9c81-44e6e6fab96f')
sourcename str

Opaque logical source identifier for the waveform data. Uniqueness is enforced as plain string equality, not canonicalised, so a file-based source reachable under two different (but equivalent) path spellings is not recognised as the same source - callers are responsible for canonicalising before storing.

required
datatype DataType

Data type of the data source.

<DataType.SAC: 'sac'>
seismogram_id UUID

Foreign key referencing the parent seismogram.

None

Attributes:

Name Type Description
seismogram AimbatSeismogram

The seismogram this data source belongs to.

Source code in src/aimbat/models/_models.py
class AimbatDataSource(SQLModel, table=True):
    """Logical source identifier and format of the waveform data for a seismogram."""

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    # Named explicitly so `create_all` (via `create_project`) and the Alembic
    # migration that added it (`72c6b97febca`) produce an identically-named
    # constraint, keeping the two schema-creation paths in sync.
    __table_args__ = (
        UniqueConstraint("sourcename", name="uq_aimbatdatasource_sourcename"),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique data source ID.",
        schema_extra={"rich": RichColSpec(style="yellow", highlight=False)},
    )
    sourcename: str = Field(
        title="Source name",
        description=(
            "Opaque logical source identifier for the waveform data. "
            + "Uniqueness is enforced as plain string equality, not "
            + "canonicalised, so a file-based source reachable under two "
            + "different (but equivalent) path spellings is not recognised "
            + "as the same source - callers are responsible for "
            + "canonicalising before storing."
        ),
    )
    datatype: DataType = Field(
        default=DataType.SAC,
        title="Data type",
        description="Data type of the data source.",
    )
    seismogram_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatseismogram.id",
        ondelete="CASCADE",
        title="Seismogram ID",
        description="Foreign key referencing the parent seismogram.",
        schema_extra={"rich": RichColSpec(style="magenta", highlight=False)},
    )
    seismogram: "AimbatSeismogram" = Relationship(back_populates="datasource")
    "The seismogram this data source belongs to."

seismogram class-attribute instance-attribute

seismogram: AimbatSeismogram = Relationship(
    back_populates="datasource"
)

The seismogram this data source belongs to.

AimbatEvent

Bases: SQLModel

Seismic event with origin time, location, and depth.

Parameters:

Name Type Description Default
id UUID

Unique event ID.

UUID('47369ad6-b521-416c-8e67-b05e497a7438')
time PydanticTimestamp

Event origin time (UTC).

required
latitude float

Event latitude in degrees.

required
longitude float

Event longitude in degrees.

required
depth float | None

Event depth in metres.

None
last_modified PydanticTimestamp | None

Timestamp of the last parameter modification.

None
stack_modified PydanticTimestamp | None

Timestamp of the last change to a parameter that requires the ICCS instance to be rebuilt (event window, ramp, bandpass, corners; per-seismogram t1, flip, select). Drives BoundICCS.is_stale. Unlike last_modified, an MCCC-only or min_cc change does not bump it.

None

Attributes:

Name Type Description
parameters AimbatEventParameters

Event parameters.

quality AimbatEventQuality | None

Live quality metrics for this event.

seismograms list[AimbatSeismogram]

List of seismograms of this event.

snapshots list[AimbatSnapshot]

List of snapshots.

Source code in src/aimbat/models/_models.py
class AimbatEvent(SQLModel, table=True):
    """Seismic event with origin time, location, and depth."""

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique event ID.",
        schema_extra={"rich": RichColSpec(style="yellow", highlight=False)},
    )
    time: PydanticTimestamp = Field(
        unique=True,
        sa_type=SAPandasTimestamp,
        title="Time",
        description="Event origin time (UTC).",
    )
    latitude: float = Field(title="Latitude", description="Event latitude in degrees.")
    longitude: float = Field(
        title="Longitude", description="Event longitude in degrees."
    )
    depth: float | None = Field(
        default=None, title="Depth", description="Event depth in metres."
    )
    last_modified: PydanticTimestamp | None = Field(
        default=None,
        sa_type=SAPandasTimestamp,
        title="Last modified",
        description="Timestamp of the last parameter modification.",
    )
    stack_modified: PydanticTimestamp | None = Field(
        default=None,
        sa_type=SAPandasTimestamp,
        title="Stack modified",
        description=(
            "Timestamp of the last change to a parameter that requires the ICCS "
            + "instance to be rebuilt (event window, ramp, bandpass, corners; "
            + "per-seismogram t1, flip, select). Drives `BoundICCS.is_stale`. "
            + "Unlike `last_modified`, an MCCC-only or `min_cc` change does not "
            + "bump it."
        ),
    )
    seismograms: list[AimbatSeismogram] = Relationship(
        back_populates="event", cascade_delete=True
    )
    "List of seismograms of this event."

    parameters: AimbatEventParameters = Relationship(
        back_populates="event", cascade_delete=True
    )
    "Event parameters."

    quality: AimbatEventQuality | None = Relationship(
        back_populates="event", cascade_delete=True
    )
    "Live quality metrics for this event."

    snapshots: list[AimbatSnapshot] = Relationship(
        back_populates="event", cascade_delete=True
    )
    "List of snapshots."

    if TYPE_CHECKING:
        # Column properties defined below, but add same default values for type checking purposes
        seismogram_count: int = 0
        station_count: int = 0
        snapshot_count: int = 0

parameters class-attribute instance-attribute

parameters: AimbatEventParameters = Relationship(
    back_populates="event", cascade_delete=True
)

Event parameters.

quality class-attribute instance-attribute

quality: AimbatEventQuality | None = Relationship(
    back_populates="event", cascade_delete=True
)

Live quality metrics for this event.

seismograms class-attribute instance-attribute

seismograms: list[AimbatSeismogram] = Relationship(
    back_populates="event", cascade_delete=True
)

List of seismograms of this event.

snapshots class-attribute instance-attribute

snapshots: list[AimbatSnapshot] = Relationship(
    back_populates="event", cascade_delete=True
)

List of snapshots.

AimbatEventParameters

Bases: AimbatEventParametersBase

Processing parameters common to all seismograms of a particular event.

Parameters:

Name Type Description Default
completed bool

Mark an event as completed.

False
ramp_width PydanticNonNegativeFloat

Width of taper ramp as a multiple of the window length. Values greater than 1 are valid; the ramp extends outside the window.

0.1
window_pre PydanticNegativeTimedelta

Pre-pick window length in seconds.

Timedelta('-1 days +23:59:45')
window_post PydanticPositiveTimedelta

Post-pick window length in seconds.

Timedelta('0 days 00:00:15')
bandpass_apply bool

Whether to apply bandpass filter to seismograms.

False
bandpass_fmin float

Minimum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

0.05
bandpass_fmax float

Maximum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

2.0
corners int

Number of corners (poles) for the bandpass filter (ignored if bandpass_apply is False).

2
min_cc float

Minimum cross-correlation used when automatically de-selecting seismograms.

0.5
mccc_damp float

Damping factor for MCCC algorithm.

0.1
mccc_min_cc float

Minimum correlation coefficient required to include a pair in the MCCC inversion.

0.5
id UUID

Unique ID.

UUID('b37d9557-66bd-4285-91c8-f2a6e542fcf9')
event_id UUID

Foreign key referencing the parent event.

None

Methods:

Name Description
check_freq_range

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Attributes:

Name Type Description
event AimbatEvent

The event these parameters belong to.

snapshots list[AimbatEventParametersSnapshot]

Parameter snapshots for this event.

Source code in src/aimbat/models/_models.py
class AimbatEventParameters(AimbatEventParametersBase, table=True):
    """Processing parameters common to all seismograms of a particular event."""

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (
        Index("ix_aimbateventparameters_event_id", "event_id", unique=True),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique ID.",
        schema_extra={
            "rich": RichColSpec(style="yellow", no_wrap=True, highlight=False)
        },
    )
    event_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatevent.id",
        ondelete="CASCADE",
        title="Event ID",
        description="Foreign key referencing the parent event.",
        schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False)
        },
    )
    event: "AimbatEvent" = Relationship(back_populates="parameters")
    "The event these parameters belong to."
    snapshots: list["AimbatEventParametersSnapshot"] = Relationship(
        back_populates="parameters", passive_deletes=True
    )
    "Parameter snapshots for this event."

event class-attribute instance-attribute

event: AimbatEvent = Relationship(
    back_populates="parameters"
)

The event these parameters belong to.

snapshots class-attribute instance-attribute

snapshots: list[AimbatEventParametersSnapshot] = (
    Relationship(
        back_populates="parameters", passive_deletes=True
    )
)

Parameter snapshots for this event.

check_freq_range

check_freq_range() -> Self

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Source code in src/aimbat/models/_parameters.py
@model_validator(mode="after")
def check_freq_range(self) -> Self:
    """Validate that `bandpass_fmax` is strictly greater than `bandpass_fmin`."""
    if self.bandpass_fmax <= self.bandpass_fmin:
        raise ValueError(
            f"bandpass_fmax ({self.bandpass_fmax:g} Hz) must be greater than "
            + f"bandpass_fmin ({self.bandpass_fmin:g} Hz). Setting one bound at "
            + "a time, widen the band first: raise bandpass_fmax before "
            + "bandpass_fmin, lower bandpass_fmin before bandpass_fmax."
        )
    return self

AimbatEventParametersBase

Bases: SQLModel

Base class defining event-level processing parameters for AIMBAT.

This class serves as a base that is inherited by the actual classes that create the database tables. It covers the settings applied uniformly to all seismograms of an event: window bounds, taper ramp width, bandpass filter settings, the CC threshold for automatic de-selection, and MCCC settings.

Parameters:

Name Type Description Default
completed bool

Mark an event as completed.

False
ramp_width PydanticNonNegativeFloat

Width of taper ramp as a multiple of the window length. Values greater than 1 are valid; the ramp extends outside the window.

0.1
window_pre PydanticNegativeTimedelta

Pre-pick window length in seconds.

Timedelta('-1 days +23:59:45')
window_post PydanticPositiveTimedelta

Post-pick window length in seconds.

Timedelta('0 days 00:00:15')
bandpass_apply bool

Whether to apply bandpass filter to seismograms.

False
bandpass_fmin float

Minimum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

0.05
bandpass_fmax float

Maximum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

2.0
corners int

Number of corners (poles) for the bandpass filter (ignored if bandpass_apply is False).

2
min_cc float

Minimum cross-correlation used when automatically de-selecting seismograms.

0.5
mccc_damp float

Damping factor for MCCC algorithm.

0.1
mccc_min_cc float

Minimum correlation coefficient required to include a pair in the MCCC inversion.

0.5

Methods:

Name Description
check_freq_range

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Source code in src/aimbat/models/_parameters.py
class AimbatEventParametersBase(SQLModel):
    """Base class defining event-level processing parameters for AIMBAT.

    This class serves as a base that is inherited by the actual classes that
    create the database tables. It covers the settings applied uniformly to
    all seismograms of an event: window bounds, taper ramp width, bandpass
    filter settings, the CC threshold for automatic de-selection, and MCCC
    settings.
    """

    completed: bool = Field(
        default=False,
        title="Completed",
        description="Mark an event as completed.",
    )

    ramp_width: PydanticNonNegativeFloat = Field(
        default_factory=lambda: settings.ramp_width,
        sa_type=Float,
        title="Ramp width",
        description=(
            "Width of taper ramp as a multiple of the window length. Values "
            + "greater than 1 are valid; the ramp extends outside the window."
        ),
    )

    window_pre: PydanticNegativeTimedelta = Field(
        sa_type=SAPandasTimedelta,
        default_factory=lambda: settings.window_pre,
        title="Window pre",
        description="Pre-pick window length in seconds.",
    )

    window_post: PydanticPositiveTimedelta = Field(
        sa_type=SAPandasTimedelta,
        default_factory=lambda: settings.window_post,
        title="Window post",
        description="Post-pick window length in seconds.",
    )

    bandpass_apply: bool = Field(
        default_factory=lambda: settings.bandpass_apply,
        title="Bandpass apply",
        description="Whether to apply bandpass filter to seismograms.",
    )

    bandpass_fmin: float = Field(
        default_factory=lambda: settings.bandpass_fmin,
        ge=0,
        title="Bandpass f min",
        description=(
            "Minimum frequency for bandpass filter in Hz (ignored if "
            + "`bandpass_apply` is False)."
        ),
    )

    bandpass_fmax: float = Field(
        default_factory=lambda: settings.bandpass_fmax,
        gt=0,
        title="Bandpass f max",
        description=(
            "Maximum frequency for bandpass filter in Hz (ignored if "
            + "`bandpass_apply` is False)."
        ),
    )

    corners: int = Field(
        default_factory=lambda: settings.corners,
        gt=0,
        title="Corners",
        description=(
            "Number of corners (poles) for the bandpass filter (ignored if "
            + "`bandpass_apply` is False)."
        ),
        sa_column_kwargs={"server_default": "2"},
    )

    min_cc: float = Field(
        ge=0.0,
        le=1.0,
        default_factory=lambda: settings.min_cc,
        title="Min CC",
        description=(
            "Minimum cross-correlation used when automatically de-selecting "
            + "seismograms."
        ),
    )

    mccc_damp: float = Field(
        default_factory=lambda: settings.mccc_damp,
        ge=0,
        title="MCCC damp",
        description="Damping factor for MCCC algorithm.",
    )

    mccc_min_cc: float = Field(
        default_factory=lambda: settings.mccc_min_cc,
        ge=0,
        le=1,
        title="MCCC min CC",
        description=(
            "Minimum correlation coefficient required to include a pair in the "
            + "MCCC inversion."
        ),
    )

    @model_validator(mode="after")
    def check_freq_range(self) -> Self:
        """Validate that `bandpass_fmax` is strictly greater than `bandpass_fmin`."""
        if self.bandpass_fmax <= self.bandpass_fmin:
            raise ValueError(
                f"bandpass_fmax ({self.bandpass_fmax:g} Hz) must be greater than "
                + f"bandpass_fmin ({self.bandpass_fmin:g} Hz). Setting one bound at "
                + "a time, widen the band first: raise bandpass_fmax before "
                + "bandpass_fmin, lower bandpass_fmin before bandpass_fmax."
            )
        return self

check_freq_range

check_freq_range() -> Self

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Source code in src/aimbat/models/_parameters.py
@model_validator(mode="after")
def check_freq_range(self) -> Self:
    """Validate that `bandpass_fmax` is strictly greater than `bandpass_fmin`."""
    if self.bandpass_fmax <= self.bandpass_fmin:
        raise ValueError(
            f"bandpass_fmax ({self.bandpass_fmax:g} Hz) must be greater than "
            + f"bandpass_fmin ({self.bandpass_fmin:g} Hz). Setting one bound at "
            + "a time, widen the band first: raise bandpass_fmax before "
            + "bandpass_fmin, lower bandpass_fmin before bandpass_fmax."
        )
    return self

AimbatEventParametersSnapshot

Bases: AimbatEventParametersBase

Snapshot of processing parameters for a particular event.

Frozen history: the source event is still reachable via snapshot.event_id, and parameters_id is set to NULL if the live parameter row is deleted.

Parameters:

Name Type Description Default
completed bool

Mark an event as completed.

False
ramp_width PydanticNonNegativeFloat

Width of taper ramp as a multiple of the window length. Values greater than 1 are valid; the ramp extends outside the window.

0.1
window_pre PydanticNegativeTimedelta

Pre-pick window length in seconds.

Timedelta('-1 days +23:59:45')
window_post PydanticPositiveTimedelta

Post-pick window length in seconds.

Timedelta('0 days 00:00:15')
bandpass_apply bool

Whether to apply bandpass filter to seismograms.

False
bandpass_fmin float

Minimum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

0.05
bandpass_fmax float

Maximum frequency for bandpass filter in Hz (ignored if bandpass_apply is False).

2.0
corners int

Number of corners (poles) for the bandpass filter (ignored if bandpass_apply is False).

2
min_cc float

Minimum cross-correlation used when automatically de-selecting seismograms.

0.5
mccc_damp float

Damping factor for MCCC algorithm.

0.1
mccc_min_cc float

Minimum correlation coefficient required to include a pair in the MCCC inversion.

0.5
id UUID

Unique ID.

UUID('24f32a5c-bde6-47e0-bbdd-b783b9a27d44')
snapshot_id UUID

Foreign key referencing the parent snapshot.

None
parameters_id UUID | None

Foreign key referencing the source event parameters, or NULL if it has been deleted.

None

Methods:

Name Description
check_freq_range

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Attributes:

Name Type Description
parameters AimbatEventParameters | None

The event parameters this snapshot was taken from, if still present.

snapshot AimbatSnapshot

The snapshot this record belongs to.

Source code in src/aimbat/models/_models.py
class AimbatEventParametersSnapshot(AimbatEventParametersBase, table=True):
    """Snapshot of processing parameters for a particular event.

    Frozen history: the source event is still reachable via
    `snapshot.event_id`, and `parameters_id` is set to `NULL` if the live
    parameter row is deleted.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    snapshot_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatsnapshot.id",
        ondelete="CASCADE",
        title="Snapshot ID",
        description="Foreign key referencing the parent snapshot.",
    )
    parameters_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbateventparameters.id",
        ondelete="SET NULL",
        title="Event parameters ID",
        description=(
            "Foreign key referencing the source event parameters, or NULL if it "
            + "has been deleted."
        ),
    )
    snapshot: "AimbatSnapshot" = Relationship(
        back_populates="event_parameters_snapshot"
    )
    "The snapshot this record belongs to."
    parameters: AimbatEventParameters | None = Relationship(back_populates="snapshots")
    "The event parameters this snapshot was taken from, if still present."

parameters class-attribute instance-attribute

parameters: AimbatEventParameters | None = Relationship(
    back_populates="snapshots"
)

The event parameters this snapshot was taken from, if still present.

snapshot class-attribute instance-attribute

snapshot: AimbatSnapshot = Relationship(
    back_populates="event_parameters_snapshot"
)

The snapshot this record belongs to.

check_freq_range

check_freq_range() -> Self

Validate that bandpass_fmax is strictly greater than bandpass_fmin.

Source code in src/aimbat/models/_parameters.py
@model_validator(mode="after")
def check_freq_range(self) -> Self:
    """Validate that `bandpass_fmax` is strictly greater than `bandpass_fmin`."""
    if self.bandpass_fmax <= self.bandpass_fmin:
        raise ValueError(
            f"bandpass_fmax ({self.bandpass_fmax:g} Hz) must be greater than "
            + f"bandpass_fmin ({self.bandpass_fmin:g} Hz). Setting one bound at "
            + "a time, widen the band first: raise bandpass_fmax before "
            + "bandpass_fmin, lower bandpass_fmin before bandpass_fmax."
        )
    return self

AimbatEventQuality

Bases: AimbatEventQualityBase

Live quality metrics for a seismic event.

One row per event. Updated in place whenever MCCC runs. Fields are None until MCCC has been executed.

Parameters:

Name Type Description Default
mccc_rmse PydanticNonNegativeTimedelta | None

Root-mean-square error of the MCCC inversion fit across the whole array.

None
id UUID

Unique ID.

UUID('525bca9d-377b-4c0f-a154-ca647d9ee21f')
event_id UUID

Foreign key referencing the parent event.

None

Attributes:

Name Type Description
event AimbatEvent

The event these quality metrics belong to.

snapshots list[AimbatEventQualitySnapshot]

Quality snapshots taken from this live record.

Source code in src/aimbat/models/_models.py
class AimbatEventQuality(AimbatEventQualityBase, table=True):
    """Live quality metrics for a seismic event.

    One row per event. Updated in place whenever MCCC runs.
    Fields are `None` until MCCC has been executed.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (Index("ix_aimbateventquality_event_id", "event_id", unique=True),)

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    event_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatevent.id",
        ondelete="CASCADE",
        title="Event ID",
        description="Foreign key referencing the parent event.",
    )
    event: "AimbatEvent" = Relationship(back_populates="quality")
    "The event these quality metrics belong to."
    snapshots: list["AimbatEventQualitySnapshot"] = Relationship(
        back_populates="quality", passive_deletes=True
    )
    "Quality snapshots taken from this live record."

event class-attribute instance-attribute

event: AimbatEvent = Relationship(back_populates='quality')

The event these quality metrics belong to.

snapshots class-attribute instance-attribute

snapshots: list[AimbatEventQualitySnapshot] = Relationship(
    back_populates="quality", passive_deletes=True
)

Quality snapshots taken from this live record.

AimbatEventQualityBase

Bases: SQLModel

Base class defining event-level quality metrics.

Fields are None when the corresponding algorithm has not been run yet.

Parameters:

Name Type Description Default
mccc_rmse PydanticNonNegativeTimedelta | None

Root-mean-square error of the MCCC inversion fit across the whole array.

None
Source code in src/aimbat/models/_quality.py
class AimbatEventQualityBase(SQLModel):
    """Base class defining event-level quality metrics.

    Fields are `None` when the corresponding algorithm has not been run yet.
    """

    mccc_rmse: PydanticNonNegativeTimedelta | None = Field(
        default=None,
        sa_type=SAPandasTimedelta,
        title="MCCC RMSE",
        description="Root-mean-square error of the MCCC inversion fit across the whole array.",
    )

AimbatEventQualitySnapshot

Bases: AimbatEventQualityBase

Snapshot of quality metrics for a seismic event.

Frozen history: the source event is still reachable via snapshot.event_id, and event_quality_id is set to NULL if the live quality row is deleted.

Parameters:

Name Type Description Default
mccc_rmse PydanticNonNegativeTimedelta | None

Root-mean-square error of the MCCC inversion fit across the whole array.

None
id UUID

Unique ID.

UUID('9f6e381a-bf6e-44c6-ba04-fc4513de7a94')
event_quality_id UUID | None

Foreign key referencing the source event quality, or NULL if it has been deleted.

None
snapshot_id UUID

Foreign key referencing the parent snapshot.

None

Attributes:

Name Type Description
quality AimbatEventQuality | None

The event quality this snapshot was taken from, if still present.

snapshot AimbatSnapshot

The snapshot this record belongs to.

Source code in src/aimbat/models/_models.py
class AimbatEventQualitySnapshot(AimbatEventQualityBase, table=True):
    """Snapshot of quality metrics for a seismic event.

    Frozen history: the source event is still reachable via
    `snapshot.event_id`, and `event_quality_id` is set to `NULL` if the live
    quality row is deleted.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    event_quality_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbateventquality.id",
        ondelete="SET NULL",
        title="Event quality ID",
        description=(
            "Foreign key referencing the source event quality, or NULL if it has "
            + "been deleted."
        ),
    )
    quality: AimbatEventQuality | None = Relationship(back_populates="snapshots")
    "The event quality this snapshot was taken from, if still present."
    snapshot_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatsnapshot.id",
        ondelete="CASCADE",
        title="Snapshot ID",
        description="Foreign key referencing the parent snapshot.",
    )
    snapshot: "AimbatSnapshot" = Relationship(back_populates="event_quality_snapshot")
    "The snapshot this record belongs to."

quality class-attribute instance-attribute

quality: AimbatEventQuality | None = Relationship(
    back_populates="snapshots"
)

The event quality this snapshot was taken from, if still present.

snapshot class-attribute instance-attribute

snapshot: AimbatSnapshot = Relationship(
    back_populates="event_quality_snapshot"
)

The snapshot this record belongs to.

AimbatEventRead

Bases: BaseModel

Read model for AimbatEvent including computed counts.

Parameters:

Name Type Description Default
id UUID

Unique identifier for the event

required
short_id str | None

Shortened unique identifier

None
completed bool

Indicates if the event's parameters are marked as completed

required
time PydanticTimestamp

Origin time of the event

required
latitude float

Latitude of the event

required
longitude float

Longitude of the event

required
depth float | None

Depth of the event

required
seismogram_count int

Number of seismograms associated with this event

required
station_count int

Number of stations associated with this event

required
snapshot_count int

Number of snapshots associated with this event

required
last_modified PydanticTimestamp | None

Timestamp of the last modification of this event's parameters

None

Methods:

Name Description
from_event

Create an AimbatEventRead from an AimbatEvent ORM instance.

Source code in src/aimbat/models/_readers.py
class AimbatEventRead(BaseModel):
    """Read model for AimbatEvent including computed counts."""

    model_config = ConfigDict(
        frozen=True,
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: UUID = Field(
        title="ID",
        description="Unique identifier for the event",
        json_schema_extra={
            "rich": RichColSpec(style="yellow"),  # type: ignore[dict-item]
        },
    )
    short_id: str | None = Field(
        default=None,
        title="Short ID",
        description="Shortened unique identifier",
        json_schema_extra={
            "tui": TuiColSpec(display_title="ID"),  # type: ignore[dict-item]
            "rich": RichColSpec(display_title="ID", style="yellow", highlight=False),  # type: ignore[dict-item]
        },
    )
    completed: bool = Field(
        title="Completed",
        description="Indicates if the event's parameters are marked as completed",
        json_schema_extra={"tui": TuiColSpec(text_align="center")},  # type: ignore[dict-item]
    )
    time: PydanticTimestamp = Field(
        title="Event time",
        description="Origin time of the event",
        json_schema_extra={
            "tui": TuiColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
            "rich": RichColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
        },
    )
    latitude: float = Field(
        title="Latitude",
        description="Latitude of the event",
    )
    longitude: float = Field(
        title="Longitude",
        description="Longitude of the event",
    )
    depth: float | None = Field(
        title="Depth",
        description="Depth of the event",
        json_schema_extra={
            "tui": TuiColSpec(display_title="Depth km", formatter=fmt_depth_km),  # type: ignore[dict-item]
            "rich": RichColSpec(
                display_title=r"Depth \[km]", justify="right", formatter=fmt_depth_km
            ),  # type: ignore[dict-item]
        },
    )
    seismogram_count: int = Field(
        title="Seismograms",
        description="Number of seismograms associated with this event",
    )
    station_count: int = Field(
        title="Stations",
        description="Number of stations associated with this event",
    )
    snapshot_count: int = Field(
        title="Snapshots",
        description="Number of snapshots associated with this event",
    )
    last_modified: PydanticTimestamp | None = Field(
        default=None,
        title="Last modified",
        description="Timestamp of the last modification of this event's parameters",
        json_schema_extra={
            "tui": TuiColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
            "rich": RichColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
        },
    )

    @classmethod
    def from_event(cls, event: AimbatEvent, session: Session | None = None) -> Self:
        """Create an AimbatEventRead from an AimbatEvent ORM instance."""
        data = event.model_dump()

        if session is not None:
            from aimbat.utils import uuid_shortener

            data["short_id"] = uuid_shortener(session, event)

        data.update(
            {
                "completed": event.parameters.completed if event.parameters else False,
                "seismogram_count": event.seismogram_count or 0,
                "station_count": event.station_count or 0,
                "snapshot_count": event.snapshot_count or 0,
            }
        )
        return cls(**data)

from_event classmethod

from_event(
    event: AimbatEvent, session: Session | None = None
) -> Self

Create an AimbatEventRead from an AimbatEvent ORM instance.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_event(cls, event: AimbatEvent, session: Session | None = None) -> Self:
    """Create an AimbatEventRead from an AimbatEvent ORM instance."""
    data = event.model_dump()

    if session is not None:
        from aimbat.utils import uuid_shortener

        data["short_id"] = uuid_shortener(session, event)

    data.update(
        {
            "completed": event.parameters.completed if event.parameters else False,
            "seismogram_count": event.seismogram_count or 0,
            "station_count": event.station_count or 0,
            "snapshot_count": event.snapshot_count or 0,
        }
    )
    return cls(**data)

AimbatNote

Bases: SQLModel

Free-text Markdown note attached to an event, station, seismogram, or snapshot.

Exactly one of the four FK fields is set per row, and at most one note exists per parent record (enforced by the per-FK partial unique indexes). Deletion of the parent record cascades to delete the note via the DB-level foreign key constraint.

Parameters:

Name Type Description Default
id UUID

Unique note ID.

UUID('e48021bc-3b21-4340-af43-feb371626aab')
content str

Note content in Markdown format.

''
event_id UUID | None

Foreign key referencing the parent event.

None
station_id UUID | None

Foreign key referencing the parent station.

None
seismogram_id UUID | None

Foreign key referencing the parent seismogram.

None
snapshot_id UUID | None

Foreign key referencing the parent snapshot.

None
Source code in src/aimbat/models/_models.py
class AimbatNote(SQLModel, table=True):
    """Free-text Markdown note attached to an event, station, seismogram, or snapshot.

    Exactly one of the four FK fields is set per row, and at most one note
    exists per parent record (enforced by the per-FK partial unique indexes).
    Deletion of the parent record cascades to delete the note via the DB-level
    foreign key constraint.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (
        CheckConstraint(
            (
                "(CASE WHEN event_id IS NOT NULL THEN 1 ELSE 0 END + CASE WHEN "
                + "station_id IS NOT NULL THEN 1 ELSE 0 END + CASE WHEN seismogram_id "
                + "IS NOT NULL THEN 1 ELSE 0 END + CASE WHEN snapshot_id IS NOT NULL "
                + "THEN 1 ELSE 0 END) = 1"
            ),
            name="aimbat_note_exactly_one_parent",
        ),
        Index(
            "ix_aimbatnote_event_id",
            "event_id",
            unique=True,
            sqlite_where=text("event_id IS NOT NULL"),
        ),
        Index(
            "ix_aimbatnote_station_id",
            "station_id",
            unique=True,
            sqlite_where=text("station_id IS NOT NULL"),
        ),
        Index(
            "ix_aimbatnote_seismogram_id",
            "seismogram_id",
            unique=True,
            sqlite_where=text("seismogram_id IS NOT NULL"),
        ),
        Index(
            "ix_aimbatnote_snapshot_id",
            "snapshot_id",
            unique=True,
            sqlite_where=text("snapshot_id IS NOT NULL"),
        ),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        description="Unique note ID.",
    )
    content: str = Field(
        default="",
        description="Note content in Markdown format.",
    )
    event_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatevent.id",
        ondelete="CASCADE",
        description="Foreign key referencing the parent event.",
    )
    station_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatstation.id",
        ondelete="CASCADE",
        description="Foreign key referencing the parent station.",
    )
    seismogram_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatseismogram.id",
        ondelete="CASCADE",
        description="Foreign key referencing the parent seismogram.",
    )
    snapshot_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatsnapshot.id",
        ondelete="CASCADE",
        description="Foreign key referencing the parent snapshot.",
    )

    @model_validator(mode="after")
    def _exactly_one_parent(self) -> "AimbatNote":
        # Table-model __init__ skips Pydantic validation, so this never runs
        # on the normal construction path; the DB CheckConstraint above is
        # the real guard. Kept for the direct-Pydantic-construction case.
        set_count = sum(
            fk is not None
            for fk in (
                self.event_id,
                self.station_id,
                self.seismogram_id,
                self.snapshot_id,
            )
        )
        if set_count != 1:
            raise ValueError(
                "Exactly one of event_id, station_id, seismogram_id, snapshot_id must"
                + " be set on AimbatNote."
            )
        return self

AimbatSeismogram

Bases: SQLModel

Seismogram metadata linking a recording station to an event.

Holds timing information (begin_time, delta, t0) and a reference to the waveform data source; the waveform samples themselves are not stored on this model but are read from that data source on demand.

Reading data returns a read-only array. Assigning data stages the write in a per-session buffer and flushes it to the data source when the owning session commits (and discards it on rollback), mirroring how ORM column changes persist. Reads through the same session see the staged value before the commit; other sessions do not. In-place mutation (seis.data[:] = ...) is not supported. Assigning data on an instance attached to no session falls back to an immediate write.

Exposes the pysmo Seismogram interface directly (begin_time, delta, data, end_time). ICCS working state (t1, flip, select) is not on this model - it lives on the linked AimbatSeismogramParameters and is reached via seismogram.parameters.

Parameters:

Name Type Description Default
id UUID

Unique seismogram ID.

UUID('21575704-1bdc-4af7-8ebc-f3f21c61111d')
begin_time PydanticTimestamp

Start time of the seismogram.

required
delta PydanticPositiveTimedelta

Sampling interval.

required
t0 PydanticTimestamp

Initial phase arrival pick.

required
station_id UUID

Foreign key referencing the recording station.

None
event_id UUID

Foreign key referencing the parent event.

None

Attributes:

Name Type Description
data NDArray[floating]

Seismogram waveform data as a read-only array.

datasource AimbatDataSource

Data source for the seismogram waveform.

end_time Timestamp

End time of the seismogram, derived from begin_time, delta, and data length.

event AimbatEvent

The event this seismogram belongs to.

parameters AimbatSeismogramParameters

Processing parameters for this seismogram.

quality AimbatSeismogramQuality | None

Live quality metrics for this seismogram.

station AimbatStation

The station that recorded this seismogram.

Source code in src/aimbat/models/_models.py
class AimbatSeismogram(SQLModel, table=True):
    """Seismogram metadata linking a recording station to an event.

    Holds timing information (`begin_time`, `delta`, `t0`) and a reference to
    the waveform data source; the waveform samples themselves are not stored
    on this model but are read from that data source on demand.

    Reading `data` returns a read-only array. Assigning `data` stages the
    write in a per-session buffer and flushes it to the data source when the
    owning session commits (and discards it on rollback), mirroring how ORM
    column changes persist. Reads through the same session see the staged
    value before the commit; other sessions do not. In-place mutation
    (`seis.data[:] = ...`) is not supported. Assigning `data` on an instance
    attached to no session falls back to an immediate write.

    Exposes the pysmo [`Seismogram`][pysmo.Seismogram] interface directly
    (`begin_time`, `delta`, `data`, `end_time`). ICCS working state (`t1`,
    `flip`, `select`) is not on this model - it lives on the linked
    `AimbatSeismogramParameters` and is reached via `seismogram.parameters`.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique seismogram ID.",
        schema_extra={"rich": RichColSpec(style="yellow", highlight=False)},
    )
    begin_time: PydanticTimestamp = Field(
        sa_type=SAPandasTimestamp,
        title="Begin time",
        description="Start time of the seismogram.",
    )
    delta: PydanticPositiveTimedelta = Field(
        sa_type=SAPandasTimedelta,
        title="Sampling interval",
        description="Sampling interval.",
    )
    t0: PydanticTimestamp = Field(
        sa_type=SAPandasTimestamp,
        title="Initial pick",
        description="Initial phase arrival pick.",
    )
    datasource: AimbatDataSource = Relationship(
        back_populates="seismogram", cascade_delete=True
    )
    "Data source for the seismogram waveform."
    station_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatstation.id",
        ondelete="CASCADE",
        title="Station ID",
        description="Foreign key referencing the recording station.",
    )
    station: "AimbatStation" = Relationship(back_populates="seismograms")
    "The station that recorded this seismogram."
    event_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatevent.id",
        ondelete="CASCADE",
        title="Event ID",
        description="Foreign key referencing the parent event.",
    )
    event: "AimbatEvent" = Relationship(back_populates="seismograms")
    "The event this seismogram belongs to."
    parameters: "AimbatSeismogramParameters" = Relationship(
        back_populates="seismogram",
        cascade_delete=True,
    )
    "Processing parameters for this seismogram."
    quality: AimbatSeismogramQuality | None = Relationship(
        back_populates="seismogram",
        cascade_delete=True,
    )
    "Live quality metrics for this seismogram."

    if TYPE_CHECKING:
        # `data` and `end_time` are provided at runtime by the properties in
        # the `else` branch; declared here so type checkers see them as
        # attributes of the model.
        data: npt.NDArray[np.floating] = np.array([])

        @property
        def end_time(self) -> Timestamp: ...

    else:

        @property
        def end_time(self) -> Timestamp:
            """End time of the seismogram, derived from begin_time, delta, and data length.

            A plain property rather than a `computed_field`: evaluating it reads
            the full waveform to count samples, so it must not be pulled into
            `model_dump()` / serialisation. Matches `pysmo.Seismogram.end_time`.
            """
            if len(self.data) == 0:
                return self.begin_time
            return self.begin_time + self.delta * (len(self.data) - 1)

        @property
        def data(self) -> npt.NDArray[np.floating]:
            """Seismogram waveform data as a read-only array.

            Assigning to this attribute stages the write; it reaches the data
            source when the owning session commits (see the class docstring).

            In-place mutation (`seis.data[:] = ...`) is not supported - the
            setter has no way to intercept a slice assignment on the array it
            already returned, so NumPy raises its own `ValueError:
            assignment destination is read-only` rather than a message
            specific to this class. Assign a new array to `seis.data` instead.
            """
            if self.datasource is None:
                raise ValueError("Expected a valid datasource name, got None.")
            return read_seismogram_data(
                self.datasource.sourcename,
                self.datasource.datatype,
                object_session(self),
            )

        @data.setter
        def data(self, value: npt.NDArray[np.floating]) -> None:
            if self.datasource is None:
                raise ValueError("Expected a valid datasource name, got None.")
            stage_seismogram_data(
                object_session(self),
                self.datasource.sourcename,
                self.datasource.datatype,
                value,
            )

data property writable

Seismogram waveform data as a read-only array.

Assigning to this attribute stages the write; it reaches the data source when the owning session commits (see the class docstring).

In-place mutation (seis.data[:] = ...) is not supported - the setter has no way to intercept a slice assignment on the array it already returned, so NumPy raises its own ValueError: assignment destination is read-only rather than a message specific to this class. Assign a new array to seis.data instead.

datasource class-attribute instance-attribute

datasource: AimbatDataSource = Relationship(
    back_populates="seismogram", cascade_delete=True
)

Data source for the seismogram waveform.

end_time property

end_time: Timestamp

End time of the seismogram, derived from begin_time, delta, and data length.

A plain property rather than a computed_field: evaluating it reads the full waveform to count samples, so it must not be pulled into model_dump() / serialisation. Matches pysmo.Seismogram.end_time.

event class-attribute instance-attribute

event: AimbatEvent = Relationship(
    back_populates="seismograms"
)

The event this seismogram belongs to.

parameters class-attribute instance-attribute

parameters: AimbatSeismogramParameters = Relationship(
    back_populates="seismogram", cascade_delete=True
)

Processing parameters for this seismogram.

quality class-attribute instance-attribute

quality: AimbatSeismogramQuality | None = Relationship(
    back_populates="seismogram", cascade_delete=True
)

Live quality metrics for this seismogram.

station class-attribute instance-attribute

station: AimbatStation = Relationship(
    back_populates="seismograms"
)

The station that recorded this seismogram.

AimbatSeismogramParameters

Bases: AimbatSeismogramParametersBase

Processing parameters for a single seismogram.

Parameters:

Name Type Description Default
flip bool

Whether or not the seismogram should be flipped.

False
select bool

Whether or not this seismogram should be used for processing.

True
t1 PydanticTimestamp | None

Working pick. This pick serves as working as well as output pick. It is changed by: 1. Picking the phase arrival in the stack, 2. Running ICCS, 3. Running MCCC.

None
id UUID

Unique ID.

UUID('4462e0a1-bc14-4077-81f1-ea4bfc65125b')
seismogram_id UUID

Foreign key referencing the parent seismogram.

None

Attributes:

Name Type Description
seismogram AimbatSeismogram

The seismogram these parameters belong to.

snapshots list[AimbatSeismogramParametersSnapshot]

Parameter snapshots for this seismogram.

Source code in src/aimbat/models/_models.py
class AimbatSeismogramParameters(AimbatSeismogramParametersBase, table=True):
    """Processing parameters for a single seismogram."""

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (
        Index(
            "ix_aimbatseismogramparameters_seismogram_id",
            "seismogram_id",
            unique=True,
        ),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique ID.",
    )
    seismogram_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatseismogram.id",
        ondelete="CASCADE",
        title="Seismogram ID",
        description="Foreign key referencing the parent seismogram.",
    )

    seismogram: "AimbatSeismogram" = Relationship(back_populates="parameters")
    "The seismogram these parameters belong to."

    snapshots: list["AimbatSeismogramParametersSnapshot"] = Relationship(
        back_populates="parameters", passive_deletes=True
    )
    "Parameter snapshots for this seismogram."

seismogram class-attribute instance-attribute

seismogram: AimbatSeismogram = Relationship(
    back_populates="parameters"
)

The seismogram these parameters belong to.

snapshots class-attribute instance-attribute

snapshots: list[AimbatSeismogramParametersSnapshot] = (
    Relationship(
        back_populates="parameters", passive_deletes=True
    )
)

Parameter snapshots for this seismogram.

AimbatSeismogramParametersBase

Bases: SQLModel

Base class defining seismogram-level processing parameters for AIMBAT.

This class serves as a base that is inherited by the actual classes that create the database tables. It covers the per-seismogram flip and select flags and the working arrival-time pick t1.

Parameters:

Name Type Description Default
flip bool

Whether or not the seismogram should be flipped.

False
select bool

Whether or not this seismogram should be used for processing.

True
t1 PydanticTimestamp | None

Working pick. This pick serves as working as well as output pick. It is changed by: 1. Picking the phase arrival in the stack, 2. Running ICCS, 3. Running MCCC.

None
Source code in src/aimbat/models/_parameters.py
class AimbatSeismogramParametersBase(SQLModel):
    """Base class defining seismogram-level processing parameters for AIMBAT.

    This class serves as a base that is inherited by the actual classes that
    create the database tables. It covers the per-seismogram `flip` and
    `select` flags and the working arrival-time pick `t1`.
    """

    flip: bool = Field(
        default=False,
        title="Flip",
        description="Whether or not the seismogram should be flipped.",
    )

    select: bool = Field(
        default=True,
        title="Select",
        description="Whether or not this seismogram should be used for processing.",
    )

    t1: PydanticTimestamp | None = Field(
        default=None,
        sa_type=SAPandasTimestamp,
        title="T1",
        description=(
            "Working pick. This pick serves as working as well as output pick. It"
            + " is changed by: 1. Picking the phase arrival in the stack, 2. "
            + "Running ICCS, 3. Running MCCC."
        ),
    )

AimbatSeismogramParametersSnapshot

Bases: AimbatSeismogramParametersBase

Snapshot of processing parameters for a single seismogram.

Frozen history: the record keeps its own seismogram_id and outlives the seismogram it was taken from. seismogram_parameters_id is set to NULL if that live parameter row is deleted.

Parameters:

Name Type Description Default
flip bool

Whether or not the seismogram should be flipped.

False
select bool

Whether or not this seismogram should be used for processing.

True
t1 PydanticTimestamp | None

Working pick. This pick serves as working as well as output pick. It is changed by: 1. Picking the phase arrival in the stack, 2. Running ICCS, 3. Running MCCC.

None
id UUID

Unique ID.

UUID('528f9664-6c8c-49f5-a25e-ab46458e51bb')
seismogram_id UUID | None

ID of the seismogram this snapshot was taken from.

None
seismogram_parameters_id UUID | None

Foreign key referencing the source seismogram parameters, or NULL if it has been deleted.

None
snapshot_id UUID

Foreign key referencing the parent snapshot.

None

Attributes:

Name Type Description
parameters AimbatSeismogramParameters | None

The seismogram parameters this snapshot was taken from, if still present.

snapshot AimbatSnapshot

The snapshot this record belongs to.

Source code in src/aimbat/models/_models.py
class AimbatSeismogramParametersSnapshot(AimbatSeismogramParametersBase, table=True):
    """Snapshot of processing parameters for a single seismogram.

    Frozen history: the record keeps its own `seismogram_id` and outlives the
    seismogram it was taken from. `seismogram_parameters_id` is set to `NULL`
    if that live parameter row is deleted.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    seismogram_id: uuid.UUID | None = Field(
        default=None,
        title="Seismogram ID",
        description="ID of the seismogram this snapshot was taken from.",
    )
    seismogram_parameters_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatseismogramparameters.id",
        ondelete="SET NULL",
        title="Seismogram parameters ID",
        description=(
            "Foreign key referencing the source seismogram parameters, or NULL if"
            + " it has been deleted."
        ),
    )
    parameters: AimbatSeismogramParameters | None = Relationship(
        back_populates="snapshots"
    )
    "The seismogram parameters this snapshot was taken from, if still present."
    snapshot_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatsnapshot.id",
        ondelete="CASCADE",
        title="Snapshot ID",
        description="Foreign key referencing the parent snapshot.",
    )
    snapshot: "AimbatSnapshot" = Relationship(
        back_populates="seismogram_parameters_snapshots"
    )
    "The snapshot this record belongs to."

parameters class-attribute instance-attribute

parameters: AimbatSeismogramParameters | None = (
    Relationship(back_populates="snapshots")
)

The seismogram parameters this snapshot was taken from, if still present.

snapshot class-attribute instance-attribute

snapshot: AimbatSnapshot = Relationship(
    back_populates="seismogram_parameters_snapshots"
)

The snapshot this record belongs to.

AimbatSeismogramQuality

Bases: AimbatSeismogramQualityBase

Live quality metrics for a single seismogram.

One row per seismogram. Updated in place whenever ICCS or MCCC runs. Fields are None until the corresponding algorithm has been executed.

Parameters:

Name Type Description Default
iccs_cc float | None

Pearson cross-correlation coefficient of this seismogram with the ICCS stack.

None
mccc_cc_mean float | None

Mean cross-correlation coefficient from the MCCC run (waveform quality).

None
mccc_cc_std float | None

Standard deviation of cross-correlation coefficients from the MCCC run (waveform consistency).

None
mccc_error PydanticNonNegativeTimedelta | None

Timing precision (standard error from covariance matrix) from the MCCC run.

None
id UUID

Unique ID.

UUID('1df8746d-dce5-4030-b289-6dfa949b4e8f')
seismogram_id UUID

Foreign key referencing the parent seismogram.

None

Attributes:

Name Type Description
seismogram AimbatSeismogram

The seismogram these quality metrics belong to.

snapshots list[AimbatSeismogramQualitySnapshot]

Quality snapshots taken from this live record.

Source code in src/aimbat/models/_models.py
class AimbatSeismogramQuality(AimbatSeismogramQualityBase, table=True):
    """Live quality metrics for a single seismogram.

    One row per seismogram. Updated in place whenever ICCS or MCCC runs.
    Fields are `None` until the corresponding algorithm has been executed.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (
        Index(
            "ix_aimbatseismogramquality_seismogram_id",
            "seismogram_id",
            unique=True,
        ),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    seismogram_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatseismogram.id",
        ondelete="CASCADE",
        title="Seismogram ID",
        description="Foreign key referencing the parent seismogram.",
    )
    seismogram: "AimbatSeismogram" = Relationship(back_populates="quality")
    "The seismogram these quality metrics belong to."
    snapshots: list["AimbatSeismogramQualitySnapshot"] = Relationship(
        back_populates="quality", passive_deletes=True
    )
    "Quality snapshots taken from this live record."

seismogram class-attribute instance-attribute

seismogram: AimbatSeismogram = Relationship(
    back_populates="quality"
)

The seismogram these quality metrics belong to.

snapshots class-attribute instance-attribute

snapshots: list[AimbatSeismogramQualitySnapshot] = (
    Relationship(
        back_populates="quality", passive_deletes=True
    )
)

Quality snapshots taken from this live record.

AimbatSeismogramQualityBase

Bases: SQLModel

Base class defining seismogram-level quality metrics.

Fields are None when the corresponding algorithm has not been run for this seismogram.

Parameters:

Name Type Description Default
iccs_cc float | None

Pearson cross-correlation coefficient of this seismogram with the ICCS stack.

None
mccc_cc_mean float | None

Mean cross-correlation coefficient from the MCCC run (waveform quality).

None
mccc_cc_std float | None

Standard deviation of cross-correlation coefficients from the MCCC run (waveform consistency).

None
mccc_error PydanticNonNegativeTimedelta | None

Timing precision (standard error from covariance matrix) from the MCCC run.

None
Source code in src/aimbat/models/_quality.py
class AimbatSeismogramQualityBase(SQLModel):
    """Base class defining seismogram-level quality metrics.

    Fields are `None` when the corresponding algorithm has not been run for
    this seismogram.
    """

    iccs_cc: float | None = Field(
        default=None,
        ge=-1.0,
        le=1.0,
        title="ICCS CC",
        description="Pearson cross-correlation coefficient of this seismogram with the ICCS stack.",
    )

    mccc_cc_mean: float | None = Field(
        default=None,
        ge=0.0,
        le=1.0,
        title="MCCC CC mean",
        description="Mean cross-correlation coefficient from the MCCC run (waveform quality).",
    )

    mccc_cc_std: float | None = Field(
        default=None,
        ge=0.0,
        title="MCCC CC std",
        description="Standard deviation of cross-correlation coefficients from the MCCC run (waveform consistency).",
    )

    mccc_error: PydanticNonNegativeTimedelta | None = Field(
        default=None,
        sa_type=SAPandasTimedelta,
        title="MCCC error",
        description="Timing precision (standard error from covariance matrix) from the MCCC run.",
    )

AimbatSeismogramQualitySnapshot

Bases: AimbatSeismogramQualityBase

Snapshot of quality metrics for a single seismogram.

Frozen history: the record keeps its own seismogram_id and outlives the seismogram it was taken from. seismogram_quality_id is set to NULL if that live quality row is deleted.

Parameters:

Name Type Description Default
iccs_cc float | None

Pearson cross-correlation coefficient of this seismogram with the ICCS stack.

None
mccc_cc_mean float | None

Mean cross-correlation coefficient from the MCCC run (waveform quality).

None
mccc_cc_std float | None

Standard deviation of cross-correlation coefficients from the MCCC run (waveform consistency).

None
mccc_error PydanticNonNegativeTimedelta | None

Timing precision (standard error from covariance matrix) from the MCCC run.

None
id UUID

Unique ID.

UUID('fde6cc3f-26f8-476d-bbaf-915a40cad4eb')
seismogram_id UUID | None

ID of the seismogram this snapshot was taken from.

None
seismogram_quality_id UUID | None

Foreign key referencing the source seismogram quality, or NULL if it has been deleted.

None
snapshot_id UUID

Foreign key referencing the parent snapshot.

None

Attributes:

Name Type Description
quality AimbatSeismogramQuality | None

The seismogram quality this snapshot was taken from, if still present.

snapshot AimbatSnapshot

The snapshot this record belongs to.

Source code in src/aimbat/models/_models.py
class AimbatSeismogramQualitySnapshot(AimbatSeismogramQualityBase, table=True):
    """Snapshot of quality metrics for a single seismogram.

    Frozen history: the record keeps its own `seismogram_id` and outlives the
    seismogram it was taken from. `seismogram_quality_id` is set to `NULL` if
    that live quality row is deleted.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    seismogram_id: uuid.UUID | None = Field(
        default=None,
        title="Seismogram ID",
        description="ID of the seismogram this snapshot was taken from.",
    )
    seismogram_quality_id: uuid.UUID | None = Field(
        default=None,
        foreign_key="aimbatseismogramquality.id",
        ondelete="SET NULL",
        title="Seismogram quality ID",
        description=(
            "Foreign key referencing the source seismogram quality, or NULL if it"
            + " has been deleted."
        ),
    )
    quality: AimbatSeismogramQuality | None = Relationship(back_populates="snapshots")
    "The seismogram quality this snapshot was taken from, if still present."
    snapshot_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatsnapshot.id",
        ondelete="CASCADE",
        title="Snapshot ID",
        description="Foreign key referencing the parent snapshot.",
    )
    snapshot: "AimbatSnapshot" = Relationship(
        back_populates="seismogram_quality_snapshots"
    )
    "The snapshot this record belongs to."

quality class-attribute instance-attribute

quality: AimbatSeismogramQuality | None = Relationship(
    back_populates="snapshots"
)

The seismogram quality this snapshot was taken from, if still present.

snapshot class-attribute instance-attribute

snapshot: AimbatSnapshot = Relationship(
    back_populates="seismogram_quality_snapshots"
)

The snapshot this record belongs to.

AimbatSeismogramRead

Bases: BaseModel

Read model for AimbatSeismogram including parameters.

Parameters:

Name Type Description Default
id UUID

Unique identifier for the seismogram

required
short_id str | None

Shortened unique identifier

None
name str

Name of the seismogram.

required
channel str

Seismogram channel.

required
select bool

Whether the seismogram is selected for processing.

required
flip bool

Whether the seismogram is flipped for processing.

required
delta_t PydanticTimedelta | None

Arrival time residual (observed - predicted) in seconds.

required
mccc_error PydanticTimedelta | None

Uncertainty in the MCCC arrival time residual (observed - predicted) in seconds.

required
iccs_cc float | None

Cross-correlation coefficient with ICCS stack.

required
mccc_cc_mean float | None

Mean cross-correlation coefficient of MCCC cluster.

required
mccc_cc_std float | None

Standard deviation of cross-correlation coefficients in MCCC cluster.

required
event_id UUID

ID of the associated event.

required
short_event_id str | None

Shortened unique identifier for the associated event.

required

Methods:

Name Description
from_seismogram

Create an AimbatSeismogramRead from an AimbatSeismogram ORM instance.

Source code in src/aimbat/models/_readers.py
class AimbatSeismogramRead(BaseModel):
    """Read model for AimbatSeismogram including parameters."""

    model_config = ConfigDict(
        frozen=True,
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: UUID = Field(
        title="ID",
        description="Unique identifier for the seismogram",
        json_schema_extra={
            "rich": RichColSpec(style="yellow", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    short_id: str | None = Field(
        default=None,
        title="Short ID",
        description="Shortened unique identifier",
        json_schema_extra={
            "tui": TuiColSpec(display_title="ID"),  # type: ignore[dict-item]
            "rich": RichColSpec(
                display_title="ID", style="yellow", no_wrap=True, highlight=False
            ),  # type: ignore[dict-item]
        },
    )
    name: str = Field(title="Name", description="Name of the seismogram.")

    channel: str = Field(
        title="Channel",
        description="Seismogram channel.",
    )

    select: bool = Field(
        title="Select",
        description="Whether the seismogram is selected for processing.",
        json_schema_extra={"tui": TuiColSpec(text_align="center")},  # type: ignore[dict-item]
    )

    flip: bool = Field(
        title="Flip",
        description="Whether the seismogram is flipped for processing.",
        json_schema_extra={
            "tui": TuiColSpec(text_align="center", formatter=fmt_flip),  # type: ignore[dict-item]
            "rich": RichColSpec(formatter=fmt_flip),  # type: ignore[dict-item]
        },
    )

    delta_t: PydanticTimedelta | None = Field(
        title="Δt (s)",
        description="Arrival time residual (observed - predicted) in seconds.",
    )

    mccc_error: PydanticTimedelta | None = Field(
        title="MCCC err Δt (s)",
        description="Uncertainty in the MCCC arrival time residual (observed - predicted) in seconds.",
    )

    iccs_cc: float | None = Field(
        title="Stack CC",
        description="Cross-correlation coefficient with ICCS stack.",
    )

    mccc_cc_mean: float | None = Field(
        title="MCCC CC",
        description="Mean cross-correlation coefficient of MCCC cluster.",
    )

    mccc_cc_std: float | None = Field(
        title="MCCC CC std",
        description="Standard deviation of cross-correlation coefficients in MCCC cluster.",
    )

    event_id: UUID = Field(
        title="Event ID",
        description="ID of the associated event.",
        json_schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )

    short_event_id: str | None = Field(
        title="Short Event ID",
        description="Shortened unique identifier for the associated event.",
        json_schema_extra={
            "rich": RichColSpec(
                display_title="Event ID", style="magenta", no_wrap=True, highlight=False
            ),  # type: ignore[dict-item]
        },
    )

    @classmethod
    def from_seismogram(
        cls, seismogram: AimbatSeismogram, session: Session | None = None
    ) -> Self:
        """Create an AimbatSeismogramRead from an AimbatSeismogram ORM instance."""
        name = (
            f"{seismogram.station.network}." if seismogram.station.network else ""
        ) + seismogram.station.name

        short_id = None
        short_event_id = None
        if session is not None:
            from aimbat.utils import uuid_shortener

            short_id = uuid_shortener(session, seismogram)
            short_event_id = uuid_shortener(session, seismogram.event)

        delta_t = (
            seismogram.parameters.t1 - seismogram.t0
            if seismogram.parameters.t1
            else None
        )
        quality = seismogram.quality
        return cls(
            id=seismogram.id,
            short_id=short_id,
            name=name,
            channel=seismogram.station.channel,
            select=seismogram.parameters.select,
            flip=seismogram.parameters.flip,
            delta_t=delta_t,
            mccc_error=quality.mccc_error if quality else None,
            iccs_cc=quality.iccs_cc if quality else None,
            mccc_cc_mean=quality.mccc_cc_mean if quality else None,
            mccc_cc_std=quality.mccc_cc_std if quality else None,
            event_id=seismogram.event_id,
            short_event_id=short_event_id,
        )

from_seismogram classmethod

from_seismogram(
    seismogram: AimbatSeismogram,
    session: Session | None = None,
) -> Self

Create an AimbatSeismogramRead from an AimbatSeismogram ORM instance.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_seismogram(
    cls, seismogram: AimbatSeismogram, session: Session | None = None
) -> Self:
    """Create an AimbatSeismogramRead from an AimbatSeismogram ORM instance."""
    name = (
        f"{seismogram.station.network}." if seismogram.station.network else ""
    ) + seismogram.station.name

    short_id = None
    short_event_id = None
    if session is not None:
        from aimbat.utils import uuid_shortener

        short_id = uuid_shortener(session, seismogram)
        short_event_id = uuid_shortener(session, seismogram.event)

    delta_t = (
        seismogram.parameters.t1 - seismogram.t0
        if seismogram.parameters.t1
        else None
    )
    quality = seismogram.quality
    return cls(
        id=seismogram.id,
        short_id=short_id,
        name=name,
        channel=seismogram.station.channel,
        select=seismogram.parameters.select,
        flip=seismogram.parameters.flip,
        delta_t=delta_t,
        mccc_error=quality.mccc_error if quality else None,
        iccs_cc=quality.iccs_cc if quality else None,
        mccc_cc_mean=quality.mccc_cc_mean if quality else None,
        mccc_cc_std=quality.mccc_cc_std if quality else None,
        event_id=seismogram.event_id,
        short_event_id=short_event_id,
    )

AimbatSnapshot

Bases: SQLModel

Container for a point-in-time snapshot of event and seismogram parameters.

An AimbatSnapshot does not store any parameter data itself. It is used to group and keep track of the associated AimbatEventParametersSnapshot and AimbatSeismogramParametersSnapshot instances.

Parameters:

Name Type Description Default
id UUID

Unique ID.

UUID('c6d92d1b-0fe3-4bc7-8752-fb5c428ee56e')
sequence int

Monotonic per-event counter; the definitive snapshot order.

required
time PydanticTimestamp

Timestamp when the snapshot was created.

Timestamp('2026-09-25 00:15:30.067907+0000', tz='UTC')
comment str | None

Optional comment for the snapshot.

None
automatic bool

Whether this snapshot was created automatically, e.g. by data add.

False
mccc_hash str | None

SHA-256 hash of the parameters affecting MCCC output at creation time.

None
iccs_hash str | None

SHA-256 hash of the parameters affecting ICCS output at creation time.

None
event_id UUID

Foreign key referencing the parent event.

None

Attributes:

Name Type Description
event AimbatEvent

The event this snapshot belongs to.

event_parameters_snapshot AimbatEventParametersSnapshot

Event parameter snapshot associated with this snapshot.

event_quality_snapshot AimbatEventQualitySnapshot | None

Event quality metric snapshot associated with this snapshot.

seismogram_parameters_snapshots list[AimbatSeismogramParametersSnapshot]

Seismogram parameter snapshots associated with this snapshot.

seismogram_quality_snapshots list[AimbatSeismogramQualitySnapshot]

Seismogram quality metric snapshots associated with this snapshot.

Source code in src/aimbat/models/_models.py
class AimbatSnapshot(SQLModel, table=True):
    """Container for a point-in-time snapshot of event and seismogram parameters.

    An `AimbatSnapshot` does not store any parameter data itself. It is used
    to group and keep track of the associated `AimbatEventParametersSnapshot`
    and `AimbatSeismogramParametersSnapshot` instances.
    """

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    __table_args__ = (
        UniqueConstraint(
            "event_id", "sequence", name="uq_aimbatsnapshot_event_id_sequence"
        ),
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4, primary_key=True, description="Unique ID."
    )
    sequence: int = Field(
        title="Sequence",
        description="Monotonic per-event counter; the definitive snapshot order.",
    )
    time: PydanticTimestamp = Field(
        default_factory=lambda: Timestamp.now("UTC"),
        sa_type=SAPandasTimestamp,
        title="Snapshot time",
        description="Timestamp when the snapshot was created.",
    )
    comment: str | None = Field(
        default=None, description="Optional comment for the snapshot."
    )
    automatic: bool = Field(
        default=False,
        description="Whether this snapshot was created automatically, e.g. by `data add`.",
    )
    mccc_hash: str | None = Field(
        default=None,
        title="MCCC hash",
        description=(
            "SHA-256 hash of the parameters affecting MCCC output at creation "
            + "time."
        ),
    )
    iccs_hash: str | None = Field(
        default=None,
        title="ICCS hash",
        description=(
            "SHA-256 hash of the parameters affecting ICCS output at creation "
            + "time."
        ),
    )
    event_parameters_snapshot: AimbatEventParametersSnapshot = Relationship(
        back_populates="snapshot", cascade_delete=True
    )
    "Event parameter snapshot associated with this snapshot."
    seismogram_parameters_snapshots: list[AimbatSeismogramParametersSnapshot] = (
        Relationship(back_populates="snapshot", cascade_delete=True)
    )
    "Seismogram parameter snapshots associated with this snapshot."
    event_quality_snapshot: AimbatEventQualitySnapshot | None = Relationship(
        back_populates="snapshot", cascade_delete=True
    )
    "Event quality metric snapshot associated with this snapshot."
    seismogram_quality_snapshots: list[AimbatSeismogramQualitySnapshot] = Relationship(
        back_populates="snapshot", cascade_delete=True
    )
    "Seismogram quality metric snapshots associated with this snapshot."
    event_id: uuid.UUID = Field(
        default=None,
        foreign_key="aimbatevent.id",
        ondelete="CASCADE",
        title="Event ID",
        description="Foreign key referencing the parent event.",
    )
    event: "AimbatEvent" = Relationship(back_populates="snapshots")
    "The event this snapshot belongs to."

    if TYPE_CHECKING:
        # defined as column properties below, but add same default values for type checking purposes
        seismogram_count: int = 0
        selected_seismogram_count: int = 0
        flipped_seismogram_count: int = 0

event class-attribute instance-attribute

event: AimbatEvent = Relationship(
    back_populates="snapshots"
)

The event this snapshot belongs to.

event_parameters_snapshot class-attribute instance-attribute

event_parameters_snapshot: AimbatEventParametersSnapshot = (
    Relationship(
        back_populates="snapshot", cascade_delete=True
    )
)

Event parameter snapshot associated with this snapshot.

event_quality_snapshot class-attribute instance-attribute

event_quality_snapshot: (
    AimbatEventQualitySnapshot | None
) = Relationship(
    back_populates="snapshot", cascade_delete=True
)

Event quality metric snapshot associated with this snapshot.

seismogram_parameters_snapshots class-attribute instance-attribute

seismogram_parameters_snapshots: list[
    AimbatSeismogramParametersSnapshot
] = Relationship(
    back_populates="snapshot", cascade_delete=True
)

Seismogram parameter snapshots associated with this snapshot.

seismogram_quality_snapshots class-attribute instance-attribute

seismogram_quality_snapshots: list[
    AimbatSeismogramQualitySnapshot
] = Relationship(
    back_populates="snapshot", cascade_delete=True
)

Seismogram quality metric snapshots associated with this snapshot.

AimbatSnapshotRead

Bases: BaseModel

Read model for AimbatSnapshot with a seismogram count.

Parameters:

Name Type Description Default
id UUID

Unique identifier for the snapshot

required
short_id str | None

Shortened unique identifier

None
sequence int

Monotonic per-event snapshot counter

required
automatic bool

Whether this snapshot was created automatically

required
time PydanticTimestamp

Timestamp of the snapshot

required
comment str | None

Optional comment for the snapshot

required
seismogram_count int

Total number of seismograms in the snapshot

required
selected_seismogram_count int

Number of selected seismograms in the snapshot

required
flipped_seismogram_count int

Number of flipped seismograms in the snapshot

required
cc_mean float | None

Mean cross-correlation coefficient for this snapshot

None
cc_sem float | None

Standard error of the mean of cross-correlation coefficients for this snapshot

None
mccc bool | None

Whether MCCC parameters are included in this snapshot

None
event_id UUID

ID of the associated event

required
short_event_id str | None

Shortened unique identifier for the associated event

required

Methods:

Name Description
from_snapshot

Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance.

Source code in src/aimbat/models/_readers.py
class AimbatSnapshotRead(BaseModel):
    """Read model for AimbatSnapshot with a seismogram count."""

    model_config = ConfigDict(
        frozen=True,
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: UUID = Field(
        title="ID",
        description="Unique identifier for the snapshot",
        json_schema_extra={
            "rich": RichColSpec(style="yellow", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )

    short_id: str | None = Field(
        default=None,
        title="Short ID",
        description="Shortened unique identifier",
        json_schema_extra={
            "tui": TuiColSpec(display_title="ID"),  # type: ignore[dict-item]
            "rich": RichColSpec(
                display_title="ID", style="yellow", no_wrap=True, highlight=False
            ),  # type: ignore[dict-item]
        },
    )

    sequence: int = Field(
        title="Seq",
        description="Monotonic per-event snapshot counter",
    )

    automatic: bool = Field(
        title="Auto", description="Whether this snapshot was created automatically"
    )

    time: PydanticTimestamp = Field(
        title="Time",
        description="Timestamp of the snapshot",
        json_schema_extra={
            "tui": TuiColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
            "rich": RichColSpec(formatter=fmt_timestamp),  # type: ignore[dict-item]
        },
    )

    comment: str | None = Field(
        title="Comment", description="Optional comment for the snapshot"
    )

    seismogram_count: int = Field(
        title="Seismograms",
        description="Total number of seismograms in the snapshot",
    )

    selected_seismogram_count: int = Field(
        title="Selected",
        description="Number of selected seismograms in the snapshot",
    )

    flipped_seismogram_count: int = Field(
        title="Flipped",
        description="Number of flipped seismograms in the snapshot",
    )

    cc_mean: float | None = Field(
        default=None,
        title="Stack CC (mean)",
        description="Mean cross-correlation coefficient for this snapshot",
    )

    cc_sem: float | None = Field(
        default=None,
        title="Stack CC (SEM)",
        description="Standard error of the mean of cross-correlation coefficients for this snapshot",
    )

    mccc: bool | None = Field(
        default=None,
        title="MCCC",
        description="Whether MCCC parameters are included in this snapshot",
    )

    event_id: UUID = Field(
        title="Event ID",
        description="ID of the associated event",
        json_schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    short_event_id: str | None = Field(
        title="Short Event ID",
        description="Shortened unique identifier for the associated event",
        json_schema_extra={
            "rich": RichColSpec(
                display_title="Event ID", style="magenta", no_wrap=True, highlight=False
            ),  # type: ignore[dict-item]
        },
    )

    @classmethod
    def from_snapshot(
        cls, snapshot: AimbatSnapshot, session: Session | None = None
    ) -> Self:
        """Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance."""

        short_id = None
        short_event_id = None
        if session is not None:
            from aimbat.utils import uuid_shortener

            short_id = uuid_shortener(session, snapshot)
            short_event_id = uuid_shortener(session, snapshot.event)

        iccs_ccs = [
            q.iccs_cc
            for q in snapshot.seismogram_quality_snapshots
            if q.iccs_cc is not None
        ]
        cc_mean, cc_sem = mean_and_sem(iccs_ccs)
        mccc = bool(snapshot.event_quality_snapshot)

        return cls(
            id=snapshot.id,
            short_id=short_id,
            sequence=snapshot.sequence,
            time=snapshot.time,
            comment=snapshot.comment,
            automatic=snapshot.automatic,
            seismogram_count=snapshot.seismogram_count,
            selected_seismogram_count=snapshot.selected_seismogram_count,
            flipped_seismogram_count=snapshot.flipped_seismogram_count,
            cc_mean=cc_mean,
            cc_sem=cc_sem,
            mccc=mccc,
            event_id=snapshot.event_id,
            short_event_id=short_event_id,
        )

from_snapshot classmethod

from_snapshot(
    snapshot: AimbatSnapshot, session: Session | None = None
) -> Self

Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_snapshot(
    cls, snapshot: AimbatSnapshot, session: Session | None = None
) -> Self:
    """Create an AimbatSnapshotRead from an AimbatSnapshot ORM instance."""

    short_id = None
    short_event_id = None
    if session is not None:
        from aimbat.utils import uuid_shortener

        short_id = uuid_shortener(session, snapshot)
        short_event_id = uuid_shortener(session, snapshot.event)

    iccs_ccs = [
        q.iccs_cc
        for q in snapshot.seismogram_quality_snapshots
        if q.iccs_cc is not None
    ]
    cc_mean, cc_sem = mean_and_sem(iccs_ccs)
    mccc = bool(snapshot.event_quality_snapshot)

    return cls(
        id=snapshot.id,
        short_id=short_id,
        sequence=snapshot.sequence,
        time=snapshot.time,
        comment=snapshot.comment,
        automatic=snapshot.automatic,
        seismogram_count=snapshot.seismogram_count,
        selected_seismogram_count=snapshot.selected_seismogram_count,
        flipped_seismogram_count=snapshot.flipped_seismogram_count,
        cc_mean=cc_mean,
        cc_sem=cc_sem,
        mccc=mccc,
        event_id=snapshot.event_id,
        short_event_id=short_event_id,
    )

AimbatStation

Bases: SQLModel

Recording station with network, location, and channel metadata.

Parameters:

Name Type Description Default
id UUID

Unique station ID.

UUID('acda2f2d-7164-4fea-8157-912f24c253bb')
name str

Station name (SEED station code).

required
network str

Network code.

required
location str

Location code.

required
channel str

Channel code (e.g. BHZ).

required
latitude float

Station latitude in degrees.

required
longitude float

Station longitude in degrees.

required
elevation float | None

Station elevation in metres.

None

Attributes:

Name Type Description
seismograms list[AimbatSeismogram]

Seismograms recorded at this station.

Source code in src/aimbat/models/_models.py
class AimbatStation(SQLModel, table=True):
    """Recording station with network, location, and channel metadata."""

    model_config = SQLModelConfig(
        alias_generator=to_camel,
        populate_by_name=True,
    )

    id: uuid.UUID = Field(
        default_factory=uuid.uuid4,
        primary_key=True,
        title="ID",
        description="Unique station ID.",
        schema_extra={"rich": RichColSpec(style="yellow", highlight=False)},
    )
    name: str = Field(
        title="Name",
        description="Station name (SEED station code).",
    )
    network: str = Field(
        title="Network",
        description="Network code.",
    )
    location: str = Field(
        title="Location",
        description="Location code.",
    )
    channel: str = Field(
        title="Channel",
        description="Channel code (e.g. BHZ).",
    )
    latitude: float = Field(
        title="Latitude", description="Station latitude in degrees."
    )
    longitude: float = Field(
        title="Longitude", description="Station longitude in degrees."
    )
    elevation: float | None = Field(
        default=None, title="Elevation", description="Station elevation in metres."
    )
    seismograms: list[AimbatSeismogram] = Relationship(
        back_populates="station", cascade_delete=True
    )
    "Seismograms recorded at this station."

    if TYPE_CHECKING:
        # Column properties defined below, but add same default values for type checking purposes
        seismogram_count: int = 0
        event_count: int = 0

seismograms class-attribute instance-attribute

seismograms: list[AimbatSeismogram] = Relationship(
    back_populates="station", cascade_delete=True
)

Seismograms recorded at this station.

AimbatStationRead

Bases: BaseModel

Read model for AimbatStation including aggregated quality statistics.

Parameters:

Name Type Description Default
id UUID

Unique identifier for the station

required
short_id str | None

Shortened unique identifier

None
network str

Station network code

required
name str

Station name

required
location str

Station location code

required
channel str

Station channel code

required
latitude float

Station latitude

required
longitude float

Station longitude

required
elevation float | None

Station elevation

None
cc_mean float | None

Mean cross-correlation coefficient at this station

None
cc_sem float | None

Standard error of the mean of cross-correlation coefficients at this station

None
seismogram_count int

Number of seismograms associated with this station

required
event_count int

Number of unique events recorded at this station

required

Methods:

Name Description
from_station

Create an AimbatStationRead from an AimbatStation ORM instance.

Source code in src/aimbat/models/_readers.py
class AimbatStationRead(BaseModel):
    """Read model for AimbatStation including aggregated quality statistics."""

    model_config = ConfigDict(
        frozen=True, alias_generator=to_camel, populate_by_name=True
    )

    id: UUID = Field(
        title="ID",
        description="Unique identifier for the station",
        json_schema_extra={
            "rich": RichColSpec(style="yellow", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    short_id: str | None = Field(
        default=None,
        title="Short ID",
        description="Shortened unique identifier",
        json_schema_extra={
            "tui": TuiColSpec(display_title="ID"),  # type: ignore[dict-item]
            "rich": RichColSpec(
                display_title="ID", style="yellow", no_wrap=True, highlight=False
            ),  # type: ignore[dict-item]
        },
    )
    network: str = Field(title="Network", description="Station network code")
    name: str = Field(title="Name", description="Station name")
    location: str = Field(title="Location", description="Station location code")
    channel: str = Field(title="Channel", description="Station channel code")
    latitude: float = Field(title="Latitude", description="Station latitude")
    longitude: float = Field(title="Longitude", description="Station longitude")
    elevation: float | None = Field(
        default=None,
        title="Elevation",
        description="Station elevation",
        json_schema_extra={
            "tui": TuiColSpec(formatter=lambda x: str(int(x))),  # type: ignore[dict-item]
            "rich": RichColSpec(formatter=lambda x: str(int(x))),  # type: ignore[dict-item]
        },
    )

    cc_mean: float | None = Field(
        default=None,
        title="Stack CC (mean)",
        description="Mean cross-correlation coefficient at this station",
    )

    cc_sem: float | None = Field(
        default=None,
        title="Stack CC (SEM)",
        description="Standard error of the mean of cross-correlation coefficients at this station",
    )

    seismogram_count: int = Field(
        title="Seismogram count",
        description="Number of seismograms associated with this station",
    )

    event_count: int = Field(
        title="Event count",
        description="Number of unique events recorded at this station",
    )

    @classmethod
    def from_station(
        cls,
        station: AimbatStation,
        session: Session | None = None,
    ) -> Self:
        """Create an AimbatStationRead from an AimbatStation ORM instance."""
        data = station.model_dump()

        if session is not None:
            from aimbat.utils import uuid_shortener

            data["short_id"] = uuid_shortener(session, station)
            iccs_ccs = tuple(
                seis.quality.iccs_cc
                for seis in station.seismograms
                if seis.quality is not None and seis.quality.iccs_cc is not None
            )
            data["cc_mean"], data["cc_sem"] = mean_and_sem(iccs_ccs)

        data.update(
            {
                "seismogram_count": station.seismogram_count or 0,
                "event_count": station.event_count or 0,
            }
        )
        return cls(**data)

from_station classmethod

from_station(
    station: AimbatStation, session: Session | None = None
) -> Self

Create an AimbatStationRead from an AimbatStation ORM instance.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_station(
    cls,
    station: AimbatStation,
    session: Session | None = None,
) -> Self:
    """Create an AimbatStationRead from an AimbatStation ORM instance."""
    data = station.model_dump()

    if session is not None:
        from aimbat.utils import uuid_shortener

        data["short_id"] = uuid_shortener(session, station)
        iccs_ccs = tuple(
            seis.quality.iccs_cc
            for seis in station.seismograms
            if seis.quality is not None and seis.quality.iccs_cc is not None
        )
        data["cc_mean"], data["cc_sem"] = mean_and_sem(iccs_ccs)

    data.update(
        {
            "seismogram_count": station.seismogram_count or 0,
            "event_count": station.event_count or 0,
        }
    )
    return cls(**data)

SeismogramQualityStats

Bases: BaseModel

Aggregated seismogram quality statistics for an event or station.

Built from persisted AimbatSeismogramQuality rows, covering both ICCS CC and MCCC diagnostics. All mean fields are None when no seismograms in the group have quality data. SEM fields are None when fewer than two values are available. mccc_rmse is only populated by from_event and from_snapshot; it is always None for from_station. event_id is populated by from_event and from_snapshot; it is always None for from_station.

For the live, DB-independent CC-only equivalent (computed straight from an ICCS instance), see aimbat.core.CcStats.

Parameters:

Name Type Description Default
event_id UUID | None
None
snapshot_id UUID | None
None
station_id UUID | None
None
count int

Number of seismograms in the group (regardless of whether they have quality data). from_event/from_station count live seismograms directly; from_snapshot uses the snapshot's own seismogram-parameter-snapshot count instead. The mean/SEM fields above only aggregate the subset that has quality data, which may be smaller than this count.

required
cc_mean float | None
None
cc_mean_sem float | None
None
mccc_cc_mean float | None
None
mccc_cc_mean_sem float | None
None
mccc_cc_std float | None
None
mccc_cc_std_sem float | None
None
mccc_error PydanticTimedelta | None
None
mccc_error_sem PydanticTimedelta | None
None
mccc_rmse PydanticTimedelta | None
None

Methods:

Name Description
from_event

Build quality stats from live quality records for an event.

from_snapshot

Build quality stats from the frozen quality records in a snapshot.

from_station

Build quality stats from live quality records for a station.

Attributes:

Name Type Description
cc_display str

ICCS CC mean ± SEM, formatted for display.

mccc_cc_mean_display str

MCCC CC mean ± SEM, formatted for display.

mccc_cc_std_display str

MCCC CC std ± SEM, formatted for display.

mccc_error_display str

MCCC arrival-time error mean ± SEM in seconds, formatted for display.

mccc_rmse_display str

MCCC RMSE in seconds, formatted for display.

Source code in src/aimbat/models/_readers.py
class SeismogramQualityStats(BaseModel):
    """Aggregated seismogram quality statistics for an event or station.

    Built from persisted `AimbatSeismogramQuality` rows, covering both ICCS
    CC and MCCC diagnostics. All mean fields are `None` when no seismograms
    in the group have quality data. SEM fields are `None` when fewer than
    two values are available. `mccc_rmse` is only populated by `from_event`
    and `from_snapshot`; it is always `None` for `from_station`. `event_id`
    is populated by `from_event` and `from_snapshot`; it is always `None`
    for `from_station`.

    For the live, DB-independent CC-only equivalent (computed straight from
    an `ICCS` instance), see `aimbat.core.CcStats`.
    """

    model_config = ConfigDict(frozen=True)

    event_id: UUID | None = Field(
        default=None,
        title="Event ID",
        json_schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    snapshot_id: UUID | None = Field(
        default=None,
        title="Snapshot ID",
        json_schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    station_id: UUID | None = Field(
        default=None,
        title="Station ID",
        json_schema_extra={
            "rich": RichColSpec(style="magenta", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    count: int = Field(
        title="Count",
        description=(
            "Number of seismograms in the group (regardless of whether they"
            + " have quality data). `from_event`/`from_station` count live"
            + " seismograms directly; `from_snapshot` uses the snapshot's own"
            + " seismogram-parameter-snapshot count instead. The mean/SEM"
            + " fields above only aggregate the subset that has quality data,"
            + " which may be smaller than this count."
        ),
    )
    cc_mean: float | None = Field(default=None, title="ICCS CC mean")
    cc_mean_sem: float | None = Field(default=None, title="ICCS CC mean SEM")
    mccc_cc_mean: float | None = Field(default=None, title="MCCC CC mean")
    mccc_cc_mean_sem: float | None = Field(default=None, title="MCCC CC mean SEM")
    mccc_cc_std: float | None = Field(default=None, title="MCCC CC std")
    mccc_cc_std_sem: float | None = Field(default=None, title="MCCC CC std SEM")
    mccc_error: PydanticTimedelta | None = Field(default=None, title="MCCC err Δt (s)")
    mccc_error_sem: PydanticTimedelta | None = Field(
        default=None, title="MCCC err Δt SEM (s)"
    )
    mccc_rmse: PydanticTimedelta | None = Field(default=None, title="MCCC RMSE (s)")

    @property
    def cc_display(self) -> str:
        """ICCS CC mean ± SEM, formatted for display."""
        return fmt_float_sem(self.cc_mean, self.cc_mean_sem)

    @property
    def mccc_cc_mean_display(self) -> str:
        """MCCC CC mean ± SEM, formatted for display."""
        return fmt_float_sem(self.mccc_cc_mean, self.mccc_cc_mean_sem)

    @property
    def mccc_cc_std_display(self) -> str:
        """MCCC CC std ± SEM, formatted for display."""
        return fmt_float_sem(self.mccc_cc_std, self.mccc_cc_std_sem)

    @property
    def mccc_error_display(self) -> str:
        """MCCC arrival-time error mean ± SEM in seconds, formatted for display."""
        return fmt_timedelta_sem(self.mccc_error, self.mccc_error_sem)

    @property
    def mccc_rmse_display(self) -> str:
        """MCCC RMSE in seconds, formatted for display."""
        return fmt_timedelta(self.mccc_rmse)

    @classmethod
    def from_event(cls, event: AimbatEvent) -> Self:
        """Build quality stats from live quality records for an event.

        Aggregates `iccs_cc` and MCCC metrics across all seismograms that
        have live quality records. `mccc_rmse` is taken from the event-level
        quality record.

        Warning:
            This method may trigger lazy-loading of `event.seismograms` and
            each `seis.quality` relationship. For performance, query `event`
            with `selectinload` for `seismograms` and their nested `quality`
            relationships before calling.

        Args:
            event: The event whose seismograms' live quality to aggregate.

        Returns:
            Aggregated quality statistics.
        """
        logger.debug(f"Building quality stats for event {event.id}.")
        qualities = [
            seis.quality for seis in event.seismograms if seis.quality is not None
        ]

        cc_mean, cc_mean_sem = mean_and_sem(
            [q.iccs_cc for q in qualities if q.iccs_cc is not None]
        )
        mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
            [q.mccc_cc_mean for q in qualities if q.mccc_cc_mean is not None]
        )
        mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
            [q.mccc_cc_std for q in qualities if q.mccc_cc_std is not None]
        )
        mccc_error, mccc_error_sem = mean_and_sem_timedelta(
            [q.mccc_error for q in qualities if q.mccc_error is not None]
        )
        mccc_rmse = event.quality.mccc_rmse if event.quality is not None else None

        return cls(
            event_id=event.id,
            count=len(event.seismograms),
            cc_mean=cc_mean,
            cc_mean_sem=cc_mean_sem,
            mccc_cc_mean=mccc_cc_mean,
            mccc_cc_mean_sem=mccc_cc_mean_sem,
            mccc_cc_std=mccc_cc_std,
            mccc_cc_std_sem=mccc_cc_std_sem,
            mccc_error=mccc_error,
            mccc_error_sem=mccc_error_sem,
            mccc_rmse=mccc_rmse,
        )

    @classmethod
    def from_station(cls, station: AimbatStation) -> Self:
        """Build quality stats from live quality records for a station.

        Aggregates `iccs_cc` and MCCC metrics across all seismograms at the
        station that have live quality records.

        Warning:
            This method may trigger lazy-loading of `station.seismograms` and
            each `seis.quality` relationship. For performance, query `station`
            with `selectinload` for `seismograms` and their nested `quality`
            relationships before calling.

        Args:
            station: The station whose seismograms' live quality to aggregate.

        Returns:
            Aggregated quality statistics. `mccc_rmse` is always `None`.
        """
        logger.debug(f"Building quality stats for station {station.id}.")
        qualities = [
            seis.quality for seis in station.seismograms if seis.quality is not None
        ]

        cc_mean, cc_mean_sem = mean_and_sem(
            [q.iccs_cc for q in qualities if q.iccs_cc is not None]
        )
        mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
            [q.mccc_cc_mean for q in qualities if q.mccc_cc_mean is not None]
        )
        mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
            [q.mccc_cc_std for q in qualities if q.mccc_cc_std is not None]
        )
        mccc_error, mccc_error_sem = mean_and_sem_timedelta(
            [q.mccc_error for q in qualities if q.mccc_error is not None]
        )

        return cls(
            station_id=station.id,
            count=len(station.seismograms),
            cc_mean=cc_mean,
            cc_mean_sem=cc_mean_sem,
            mccc_cc_mean=mccc_cc_mean,
            mccc_cc_mean_sem=mccc_cc_mean_sem,
            mccc_cc_std=mccc_cc_std,
            mccc_cc_std_sem=mccc_cc_std_sem,
            mccc_error=mccc_error,
            mccc_error_sem=mccc_error_sem,
            mccc_rmse=None,
        )

    @classmethod
    def from_snapshot(cls, snapshot: AimbatSnapshot) -> Self:
        """Build quality stats from the frozen quality records in a snapshot.

        Aggregates from `AimbatSeismogramQualitySnapshot` records rather than
        live quality, so the result reflects the state at snapshot time.

        Args:
            snapshot: The snapshot to aggregate quality from.

        Returns:
            Aggregated quality statistics.
        """
        logger.debug(f"Building quality stats for snapshot {snapshot.id}.")
        records = snapshot.seismogram_quality_snapshots

        cc_mean, cc_mean_sem = mean_and_sem(
            [r.iccs_cc for r in records if r.iccs_cc is not None]
        )
        mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
            [r.mccc_cc_mean for r in records if r.mccc_cc_mean is not None]
        )
        mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
            [r.mccc_cc_std for r in records if r.mccc_cc_std is not None]
        )
        mccc_error, mccc_error_sem = mean_and_sem_timedelta(
            [r.mccc_error for r in records if r.mccc_error is not None]
        )
        eq = snapshot.event_quality_snapshot
        mccc_rmse = eq.mccc_rmse if eq is not None else None

        return cls(
            event_id=snapshot.event_id,
            snapshot_id=snapshot.id,
            count=snapshot.seismogram_count,
            cc_mean=cc_mean,
            cc_mean_sem=cc_mean_sem,
            mccc_cc_mean=mccc_cc_mean,
            mccc_cc_mean_sem=mccc_cc_mean_sem,
            mccc_cc_std=mccc_cc_std,
            mccc_cc_std_sem=mccc_cc_std_sem,
            mccc_error=mccc_error,
            mccc_error_sem=mccc_error_sem,
            mccc_rmse=mccc_rmse,
        )

cc_display property

cc_display: str

ICCS CC mean ± SEM, formatted for display.

mccc_cc_mean_display property

mccc_cc_mean_display: str

MCCC CC mean ± SEM, formatted for display.

mccc_cc_std_display property

mccc_cc_std_display: str

MCCC CC std ± SEM, formatted for display.

mccc_error_display property

mccc_error_display: str

MCCC arrival-time error mean ± SEM in seconds, formatted for display.

mccc_rmse_display property

mccc_rmse_display: str

MCCC RMSE in seconds, formatted for display.

from_event classmethod

from_event(event: AimbatEvent) -> Self

Build quality stats from live quality records for an event.

Aggregates iccs_cc and MCCC metrics across all seismograms that have live quality records. mccc_rmse is taken from the event-level quality record.

Warning

This method may trigger lazy-loading of event.seismograms and each seis.quality relationship. For performance, query event with selectinload for seismograms and their nested quality relationships before calling.

Parameters:

Name Type Description Default
event AimbatEvent

The event whose seismograms' live quality to aggregate.

required

Returns:

Type Description
Self

Aggregated quality statistics.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_event(cls, event: AimbatEvent) -> Self:
    """Build quality stats from live quality records for an event.

    Aggregates `iccs_cc` and MCCC metrics across all seismograms that
    have live quality records. `mccc_rmse` is taken from the event-level
    quality record.

    Warning:
        This method may trigger lazy-loading of `event.seismograms` and
        each `seis.quality` relationship. For performance, query `event`
        with `selectinload` for `seismograms` and their nested `quality`
        relationships before calling.

    Args:
        event: The event whose seismograms' live quality to aggregate.

    Returns:
        Aggregated quality statistics.
    """
    logger.debug(f"Building quality stats for event {event.id}.")
    qualities = [
        seis.quality for seis in event.seismograms if seis.quality is not None
    ]

    cc_mean, cc_mean_sem = mean_and_sem(
        [q.iccs_cc for q in qualities if q.iccs_cc is not None]
    )
    mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
        [q.mccc_cc_mean for q in qualities if q.mccc_cc_mean is not None]
    )
    mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
        [q.mccc_cc_std for q in qualities if q.mccc_cc_std is not None]
    )
    mccc_error, mccc_error_sem = mean_and_sem_timedelta(
        [q.mccc_error for q in qualities if q.mccc_error is not None]
    )
    mccc_rmse = event.quality.mccc_rmse if event.quality is not None else None

    return cls(
        event_id=event.id,
        count=len(event.seismograms),
        cc_mean=cc_mean,
        cc_mean_sem=cc_mean_sem,
        mccc_cc_mean=mccc_cc_mean,
        mccc_cc_mean_sem=mccc_cc_mean_sem,
        mccc_cc_std=mccc_cc_std,
        mccc_cc_std_sem=mccc_cc_std_sem,
        mccc_error=mccc_error,
        mccc_error_sem=mccc_error_sem,
        mccc_rmse=mccc_rmse,
    )

from_snapshot classmethod

from_snapshot(snapshot: AimbatSnapshot) -> Self

Build quality stats from the frozen quality records in a snapshot.

Aggregates from AimbatSeismogramQualitySnapshot records rather than live quality, so the result reflects the state at snapshot time.

Parameters:

Name Type Description Default
snapshot AimbatSnapshot

The snapshot to aggregate quality from.

required

Returns:

Type Description
Self

Aggregated quality statistics.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_snapshot(cls, snapshot: AimbatSnapshot) -> Self:
    """Build quality stats from the frozen quality records in a snapshot.

    Aggregates from `AimbatSeismogramQualitySnapshot` records rather than
    live quality, so the result reflects the state at snapshot time.

    Args:
        snapshot: The snapshot to aggregate quality from.

    Returns:
        Aggregated quality statistics.
    """
    logger.debug(f"Building quality stats for snapshot {snapshot.id}.")
    records = snapshot.seismogram_quality_snapshots

    cc_mean, cc_mean_sem = mean_and_sem(
        [r.iccs_cc for r in records if r.iccs_cc is not None]
    )
    mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
        [r.mccc_cc_mean for r in records if r.mccc_cc_mean is not None]
    )
    mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
        [r.mccc_cc_std for r in records if r.mccc_cc_std is not None]
    )
    mccc_error, mccc_error_sem = mean_and_sem_timedelta(
        [r.mccc_error for r in records if r.mccc_error is not None]
    )
    eq = snapshot.event_quality_snapshot
    mccc_rmse = eq.mccc_rmse if eq is not None else None

    return cls(
        event_id=snapshot.event_id,
        snapshot_id=snapshot.id,
        count=snapshot.seismogram_count,
        cc_mean=cc_mean,
        cc_mean_sem=cc_mean_sem,
        mccc_cc_mean=mccc_cc_mean,
        mccc_cc_mean_sem=mccc_cc_mean_sem,
        mccc_cc_std=mccc_cc_std,
        mccc_cc_std_sem=mccc_cc_std_sem,
        mccc_error=mccc_error,
        mccc_error_sem=mccc_error_sem,
        mccc_rmse=mccc_rmse,
    )

from_station classmethod

from_station(station: AimbatStation) -> Self

Build quality stats from live quality records for a station.

Aggregates iccs_cc and MCCC metrics across all seismograms at the station that have live quality records.

Warning

This method may trigger lazy-loading of station.seismograms and each seis.quality relationship. For performance, query station with selectinload for seismograms and their nested quality relationships before calling.

Parameters:

Name Type Description Default
station AimbatStation

The station whose seismograms' live quality to aggregate.

required

Returns:

Type Description
Self

Aggregated quality statistics. mccc_rmse is always None.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_station(cls, station: AimbatStation) -> Self:
    """Build quality stats from live quality records for a station.

    Aggregates `iccs_cc` and MCCC metrics across all seismograms at the
    station that have live quality records.

    Warning:
        This method may trigger lazy-loading of `station.seismograms` and
        each `seis.quality` relationship. For performance, query `station`
        with `selectinload` for `seismograms` and their nested `quality`
        relationships before calling.

    Args:
        station: The station whose seismograms' live quality to aggregate.

    Returns:
        Aggregated quality statistics. `mccc_rmse` is always `None`.
    """
    logger.debug(f"Building quality stats for station {station.id}.")
    qualities = [
        seis.quality for seis in station.seismograms if seis.quality is not None
    ]

    cc_mean, cc_mean_sem = mean_and_sem(
        [q.iccs_cc for q in qualities if q.iccs_cc is not None]
    )
    mccc_cc_mean, mccc_cc_mean_sem = mean_and_sem(
        [q.mccc_cc_mean for q in qualities if q.mccc_cc_mean is not None]
    )
    mccc_cc_std, mccc_cc_std_sem = mean_and_sem(
        [q.mccc_cc_std for q in qualities if q.mccc_cc_std is not None]
    )
    mccc_error, mccc_error_sem = mean_and_sem_timedelta(
        [q.mccc_error for q in qualities if q.mccc_error is not None]
    )

    return cls(
        station_id=station.id,
        count=len(station.seismograms),
        cc_mean=cc_mean,
        cc_mean_sem=cc_mean_sem,
        mccc_cc_mean=mccc_cc_mean,
        mccc_cc_mean_sem=mccc_cc_mean_sem,
        mccc_cc_std=mccc_cc_std,
        mccc_cc_std_sem=mccc_cc_std_sem,
        mccc_error=mccc_error,
        mccc_error_sem=mccc_error_sem,
        mccc_rmse=None,
    )

SnapshotResults

Bases: BaseModel

Full results export for a snapshot.

Contains event- and snapshot-level header information followed by per-seismogram result records. All repeated scalars (UUIDs, event details, MCCC RMSE) appear once in the envelope rather than being duplicated across every row.

Depth is exposed in metres, matching the database column.

Parameters:

Name Type Description Default
snapshot_id UUID
required
snapshot_time PydanticTimestamp
required
snapshot_comment str | None
None
event_id UUID
required
event_time PydanticTimestamp
required
event_latitude float
required
event_longitude float
required
event_depth float | None

Event depth in metres.

None
mccc_rmse PydanticTimedelta | None

Global MCCC root-mean-square error for the event.

None
seismograms list[SnapshotSeismogramResult]

Per-seismogram result records.

required
Source code in src/aimbat/models/_readers.py
class SnapshotResults(BaseModel):
    """Full results export for a snapshot.

    Contains event- and snapshot-level header information followed by
    per-seismogram result records. All repeated scalars (UUIDs, event
    details, MCCC RMSE) appear once in the envelope rather than being
    duplicated across every row.

    Depth is exposed in metres, matching the database column.
    """

    model_config = ConfigDict(
        frozen=True,
        alias_generator=to_camel,
        populate_by_name=True,
    )

    snapshot_id: UUID = Field(title="Snapshot ID")
    snapshot_time: PydanticTimestamp = Field(title="Snapshot time")
    snapshot_comment: str | None = Field(default=None, title="Snapshot comment")
    event_id: UUID = Field(title="Event ID")
    event_time: PydanticTimestamp = Field(title="Event time")
    event_latitude: float = Field(title="Event latitude")
    event_longitude: float = Field(title="Event longitude")
    event_depth: float | None = Field(
        default=None,
        title="Event depth (m)",
        description="Event depth in metres.",
    )
    mccc_rmse: PydanticTimedelta | None = Field(
        default=None,
        title="MCCC RMSE (s)",
        description="Global MCCC root-mean-square error for the event.",
    )
    seismograms: list[SnapshotSeismogramResult] = Field(
        title="Seismograms",
        description="Per-seismogram result records.",
    )

SnapshotSeismogramResult

Bases: BaseModel

Per-seismogram result record from a snapshot.

Joins the frozen parameter and quality records from a snapshot to produce one row per seismogram. Only snapshots with MCCC data will have meaningful MCCC columns; iccs_cc is populated whenever ICCS was run before the snapshot was taken.

Event- and snapshot-level scalars (snapshot_id, event_id, mccc_rmse) are not repeated here; they live in the enclosing SnapshotResults envelope.

Parameters:

Name Type Description Default
seismogram_id UUID | None
None
name str

Station network and name.

required
channel str

Station channel code.

required
select bool

Whether this seismogram was selected at snapshot time.

required
flip bool

Whether this seismogram was flipped at snapshot time.

required
t1 PydanticTimestamp | None

Frozen pick time (absolute timestamp) at snapshot time.

None
iccs_cc float | None

Cross-correlation coefficient with ICCS stack at snapshot time.

None
mccc_cc_mean float | None

Mean cross-correlation coefficient of MCCC cluster.

None
mccc_cc_std float | None

Standard deviation of cross-correlation coefficients in MCCC cluster.

None
mccc_error PydanticTimedelta | None

Uncertainty in the MCCC arrival time residual in seconds.

None

Methods:

Name Description
from_snapshot_records

Build a result record from pre-loaded snapshot records.

Source code in src/aimbat/models/_readers.py
class SnapshotSeismogramResult(BaseModel):
    """Per-seismogram result record from a snapshot.

    Joins the frozen parameter and quality records from a snapshot to produce
    one row per seismogram. Only snapshots with MCCC data will have meaningful
    MCCC columns; `iccs_cc` is populated whenever ICCS was run before the
    snapshot was taken.

    Event- and snapshot-level scalars (`snapshot_id`, `event_id`, `mccc_rmse`)
    are not repeated here; they live in the enclosing `SnapshotResults` envelope.
    """

    model_config = ConfigDict(
        frozen=True,
        alias_generator=to_camel,
        populate_by_name=True,
    )

    seismogram_id: UUID | None = Field(
        default=None,
        title="Seismogram ID",
        json_schema_extra={
            "rich": RichColSpec(style="yellow", no_wrap=True, highlight=False),  # type: ignore[dict-item]
        },
    )
    name: str = Field(title="Name", description="Station network and name.")
    channel: str = Field(title="Channel", description="Station channel code.")
    select: bool = Field(
        title="Select",
        description="Whether this seismogram was selected at snapshot time.",
    )
    flip: bool = Field(
        title="Flip",
        description="Whether this seismogram was flipped at snapshot time.",
    )
    t1: PydanticTimestamp | None = Field(
        default=None,
        title="T1",
        description="Frozen pick time (absolute timestamp) at snapshot time.",
    )
    iccs_cc: float | None = Field(
        default=None,
        title="Stack CC",
        description="Cross-correlation coefficient with ICCS stack at snapshot time.",
    )
    mccc_cc_mean: float | None = Field(
        default=None,
        title="MCCC CC",
        description="Mean cross-correlation coefficient of MCCC cluster.",
    )
    mccc_cc_std: float | None = Field(
        default=None,
        title="MCCC CC std",
        description="Standard deviation of cross-correlation coefficients in MCCC cluster.",
    )
    mccc_error: PydanticTimedelta | None = Field(
        default=None,
        title="MCCC err Δt (s)",
        description="Uncertainty in the MCCC arrival time residual in seconds.",
    )

    @classmethod
    def from_snapshot_records(
        cls,
        param_snap: AimbatSeismogramParametersSnapshot,
        quality_snap: AimbatSeismogramQualitySnapshot | None,
        live_seismogram: AimbatSeismogram | None,
    ) -> SnapshotSeismogramResult:
        """Build a result record from pre-loaded snapshot records.

        Warning:
            `live_seismogram.station` must be loaded before calling (e.g. via
            `selectinload`) when `live_seismogram` is not `None`.

        Args:
            param_snap: Seismogram parameters snapshot record.
            quality_snap: Matching seismogram quality snapshot, or `None` if
                no quality data were captured for this seismogram.
            live_seismogram: The live seismogram the snapshot was taken from,
                or `None` if it has since been deleted.

        Returns:
            Assembled result record.
        """
        if live_seismogram is not None:
            station = live_seismogram.station
            name = (f"{station.network}." if station.network else "") + station.name
            channel = station.channel
        else:
            name = "(deleted)"
            channel = ""
        return cls(
            seismogram_id=param_snap.seismogram_id,
            name=name,
            channel=channel,
            select=param_snap.select,
            flip=param_snap.flip,
            t1=param_snap.t1,
            iccs_cc=quality_snap.iccs_cc if quality_snap else None,
            mccc_cc_mean=quality_snap.mccc_cc_mean if quality_snap else None,
            mccc_cc_std=quality_snap.mccc_cc_std if quality_snap else None,
            mccc_error=quality_snap.mccc_error if quality_snap else None,
        )

from_snapshot_records classmethod

from_snapshot_records(
    param_snap: AimbatSeismogramParametersSnapshot,
    quality_snap: AimbatSeismogramQualitySnapshot | None,
    live_seismogram: AimbatSeismogram | None,
) -> SnapshotSeismogramResult

Build a result record from pre-loaded snapshot records.

Warning

live_seismogram.station must be loaded before calling (e.g. via selectinload) when live_seismogram is not None.

Parameters:

Name Type Description Default
param_snap AimbatSeismogramParametersSnapshot

Seismogram parameters snapshot record.

required
quality_snap AimbatSeismogramQualitySnapshot | None

Matching seismogram quality snapshot, or None if no quality data were captured for this seismogram.

required
live_seismogram AimbatSeismogram | None

The live seismogram the snapshot was taken from, or None if it has since been deleted.

required

Returns:

Type Description
SnapshotSeismogramResult

Assembled result record.

Source code in src/aimbat/models/_readers.py
@classmethod
def from_snapshot_records(
    cls,
    param_snap: AimbatSeismogramParametersSnapshot,
    quality_snap: AimbatSeismogramQualitySnapshot | None,
    live_seismogram: AimbatSeismogram | None,
) -> SnapshotSeismogramResult:
    """Build a result record from pre-loaded snapshot records.

    Warning:
        `live_seismogram.station` must be loaded before calling (e.g. via
        `selectinload`) when `live_seismogram` is not `None`.

    Args:
        param_snap: Seismogram parameters snapshot record.
        quality_snap: Matching seismogram quality snapshot, or `None` if
            no quality data were captured for this seismogram.
        live_seismogram: The live seismogram the snapshot was taken from,
            or `None` if it has since been deleted.

    Returns:
        Assembled result record.
    """
    if live_seismogram is not None:
        station = live_seismogram.station
        name = (f"{station.network}." if station.network else "") + station.name
        channel = station.channel
    else:
        name = "(deleted)"
        channel = ""
    return cls(
        seismogram_id=param_snap.seismogram_id,
        name=name,
        channel=channel,
        select=param_snap.select,
        flip=param_snap.flip,
        t1=param_snap.t1,
        iccs_cc=quality_snap.iccs_cc if quality_snap else None,
        mccc_cc_mean=quality_snap.mccc_cc_mean if quality_snap else None,
        mccc_cc_std=quality_snap.mccc_cc_std if quality_snap else None,
        mccc_error=quality_snap.mccc_error if quality_snap else None,
    )

undefer_counts

undefer_counts(
    model_class: type[SQLModel],
) -> tuple[Any, ...]

Loader options that fetch model_class's count columns in the main query.

Pass these to select(...).options(...) in a path that renders counts for many rows; without them each count is a query of its own, per row.

The options are read off the mapper rather than listed here, so a count added later reaches every table-rendering path without any of them being touched.

Source code in src/aimbat/models/_models.py
def undefer_counts(model_class: type[SQLModel]) -> tuple[Any, ...]:
    """Loader options that fetch `model_class`'s count columns in the main query.

    Pass these to `select(...).options(...)` in a path that renders counts for
    many rows; without them each count is a query of its own, per row.

    The options are read off the mapper rather than listed here, so a count
    added later reaches every table-rendering path without any of them being
    touched.
    """
    return tuple(
        undefer(getattr(model_class, prop.key))
        for prop in class_mapper(model_class).column_attrs
        if prop.deferred
    )