Skip to content

aimbat.core

Core logic for AIMBAT.

All functions take a SQLModel Session and work with the models in aimbat.models. The main areas covered are:

  • Data — add data to the project, linking each source to its station, event, and seismogram records (add_data_to_project).
  • Events, seismograms, stations — query, update, and delete records; read and write parameters; resolve an event from an explicit ID (resolve_event).
  • ICCS / MCCC — run the Iterative Cross-Correlation and Stack (run_iccs) and Multi-Channel Cross-Correlation (run_mccc) algorithms; update picks, time windows, and correlation thresholds.
  • Snapshots — save, restore, and delete parameter snapshots (create_snapshot, rollback_to_snapshot).
  • Project — create and delete the project database (create_project, delete_project).
  • Migrations — bring an existing project database up to date with the current schema via Alembic (upgrade_project, get_current_revision).

Classes:

Name Description
BoundICCS

An ICCS instance explicitly bound to a specific event.

CcStats

Live CC summary statistics for an ICCS instance.

IccsLifecycle

Framework-agnostic state and policy for keeping a BoundICCS up to date.

IccsValidationError

Raised when an event's parameters cannot produce an ICCS instance.

NoSeismogramsError

Raised when an ICCS instance is requested for an event with no seismograms.

PruneReport

What prune_project found and (unless dry_run) did.

SchemaMismatchError

An un-stamped database's live schema doesn't match a known revision.

SchemaStaleWarning

A project database's schema is behind the latest Alembic revision.

SyncResult

Which live quality metrics sync_from_matching_hash repopulated.

Functions:

Name Description
add_data_to_project

Add data sources to the AIMBAT database.

add_seismograms_to_project

Ingest already-built (seismogram, station, event) triples directly.

build_iccs_from_snapshot

Build a read-only BoundICCS from a snapshot's parameters and live waveform data.

cc_stats

Summarise live CC values (mean ± SEM) across all and selected seismograms.

clear_iccs_cache

Clear the process-level ICCS cache.

clear_mccc_quality

Clear MCCC quality metrics from the live quality tables for an event.

compute_iccs_hash

Compute the hash identifying the ICCS stack these parameters produce.

compute_mccc_hash

Compute the hash identifying the MCCC inversion these parameters produce.

create_iccs_instance

Return a BoundICCS instance for the given event.

create_project

Initialise a new AIMBAT project database schema and triggers.

create_snapshot

Create a snapshot of the AIMBAT processing parameters and quality metrics.

create_snapshots_for_added_data

Create one snapshot per event that received a newly created seismogram.

delete_event

Delete an AimbatEvent from the database.

delete_project

Delete the AIMBAT project.

delete_seismogram

Delete an AimbatSeismogram from the database.

delete_snapshot

Delete an AIMBAT parameter snapshot.

delete_station

Delete an AimbatStation from the database.

dump_data_table

Return AIMBAT datasources table as a JSON-serialisable list of dicts.

dump_event_parameter_snapshot_table

Dump event parameter snapshots as a list of dicts.

dump_event_parameter_table

Dump the event parameter table data to json.

dump_event_quality_snapshot_table

Dump event quality snapshots as a list of dicts.

dump_event_quality_table

Dump event quality statistics to json.

dump_event_table

Dump the table data to a JSON-serialisable list of dicts.

dump_seismogram_parameter_snapshot_table

Dump seismogram parameter snapshots as a list of dicts.

dump_seismogram_parameter_table

Dump the seismogram parameter table data to json serialisable list of dicts.

dump_seismogram_quality_snapshot_table

Dump seismogram quality snapshots as a list of dicts.

dump_seismogram_table

Dump the AimbatSeismogram table to json serialisable list of dicts.

dump_snapshot_quality_table

Dump snapshot quality statistics to json.

dump_snapshot_results

Dump per-seismogram MCCC results from a snapshot as a results envelope.

dump_snapshot_table

Dump snapshot metadata as a list of dicts.

dump_station_quality_table

Dump station quality statistics to json.

dump_station_table

Create a JSON serialisable dict from the AimbatStation table data.

evict_iccs_cache_entry

Remove one event's cached ICCS instance, if present.

get_completed_events

Get the events marked as completed.

get_current_revision

Return the Alembic revision engine's database is stamped at.

get_data_for_event

Return the data sources belonging to the given event.

get_event_quality

Get aggregated quality statistics for an event.

get_events_using_station

Get all events that use a particular station.

get_head_revision

Return the latest revision ID available in AIMBAT's migration scripts.

get_note_content

Return the note content for the given entity.

get_selected_seismograms

Get the selected seismograms for the given event.

get_snapshot_quality

Get aggregated quality statistics for a snapshot.

get_snapshots

Get the snapshots, optional filtered by event ID.

get_station_iccs_ccs

Get ICCS cross-correlation coefficients for all seismograms of a station across all events.

get_station_quality

Get aggregated quality statistics for a station.

get_stations_in_event

Get the stations for a particular event.

prune_project

Delete seismograms whose data source can no longer be read.

reset_seismogram_parameters

Reset an AimbatSeismogram's parameters to their default values.

resolve_event

Resolve an event from an explicit ID.

resync_quality

Bring an event's live quality metrics back in line with its parameters.

rollback_to_snapshot

Rollback to an AIMBAT parameters snapshot.

run_iccs

Run the Iterative Cross-Correlation and Stack (ICCS) algorithm.

run_mccc

Run the Multi-Channel Cross-Correlation (MCCC) algorithm.

save_note

Save note content for the given entity, creating the note record if needed.

set_event_parameter

Set event parameter value for the given event.

set_event_parameters

Set several event parameters at once, validated as a group.

set_seismogram_parameter

Set parameter value for an AimbatSeismogram instance.

stamp_head

Mark a database as being at the latest Alembic revision.

sync_from_matching_hash

Repopulate an event's live quality metrics from matching snapshots.

sync_iccs_parameters

Sync an existing ICCS instance's parameters from the database.

toggle_event_completed

Flip the completed flag on an event's parameters.

upgrade_project

Upgrade the project database to the latest Alembic revision.

validate_iccs_construction

Try to construct an ICCS instance for the event without caching the result.

write_back_seismograms

Write t1, flip, and select from ICCS seismograms back to the database.

BoundICCS dataclass

An ICCS instance explicitly bound to a specific event.

Use is_stale to detect whether a stack-affecting parameter has been modified (e.g. by a CLI command) since this instance was created.

Parameters:

Name Type Description Default
iccs ICCS
required
event_id UUID
required
created_at Timestamp
required

Methods:

Name Description
is_stale

Return True if this ICCS instance no longer matches the event.

Source code in src/aimbat/core/_iccs.py
@dataclass
class BoundICCS:
    """An ICCS instance explicitly bound to a specific event.

    Use `is_stale` to detect whether a stack-affecting parameter has been
    modified (e.g. by a CLI command) since this instance was created.
    """

    iccs: ICCS
    event_id: UUID
    created_at: Timestamp

    def is_stale(self, event: AimbatEvent) -> bool:
        """Return True if this ICCS instance no longer matches the event.

        Args:
            event: The event to check against.

        Returns:
            True if `event.id` differs from the bound event, or if
            `event.stack_modified` has advanced since this instance was
            created. An MCCC-only or `min_cc` change does not make it stale
            (`run_iccs` refreshes `min_cc` on the instance per run).
        """
        if event.id != self.event_id:
            return True
        if event.stack_modified is None:
            return False
        # The triggers stamp `stack_modified` to millisecond precision while
        # `created_at` keeps microseconds, so the two are compared at the
        # coarser one. A change landing in the same millisecond counts as
        # stale: the worst case is one redundant rebuild, against silently
        # serving values the database has already superseded.
        return event.stack_modified >= self.created_at.floor("ms")

is_stale

is_stale(event: AimbatEvent) -> bool

Return True if this ICCS instance no longer matches the event.

Parameters:

Name Type Description Default
event AimbatEvent

The event to check against.

required

Returns:

Type Description
bool

True if event.id differs from the bound event, or if

bool

event.stack_modified has advanced since this instance was

bool

created. An MCCC-only or min_cc change does not make it stale

bool

(run_iccs refreshes min_cc on the instance per run).

Source code in src/aimbat/core/_iccs.py
def is_stale(self, event: AimbatEvent) -> bool:
    """Return True if this ICCS instance no longer matches the event.

    Args:
        event: The event to check against.

    Returns:
        True if `event.id` differs from the bound event, or if
        `event.stack_modified` has advanced since this instance was
        created. An MCCC-only or `min_cc` change does not make it stale
        (`run_iccs` refreshes `min_cc` on the instance per run).
    """
    if event.id != self.event_id:
        return True
    if event.stack_modified is None:
        return False
    # The triggers stamp `stack_modified` to millisecond precision while
    # `created_at` keeps microseconds, so the two are compared at the
    # coarser one. A change landing in the same millisecond counts as
    # stale: the worst case is one redundant rebuild, against silently
    # serving values the database has already superseded.
    return event.stack_modified >= self.created_at.floor("ms")

CcStats dataclass

Live CC summary statistics for an ICCS instance.

mean_selected/sem_selected are computed from seismograms with select=True only; mean_all/sem_all from every seismogram. SEM fields are None when fewer than two values are available.

Computed directly from ICCS.ccs against the current stack, so it is always up to date without a DB round-trip. For the persisted-quality equivalent (event/station/snapshot scoped, DB-backed), see aimbat.models.SeismogramQualityStats.

Parameters:

Name Type Description Default
n_all int
required
mean_all float | None
required
sem_all float | None
required
n_selected int
required
mean_selected float | None
required
sem_selected float | None
required
Source code in src/aimbat/core/_iccs.py
@dataclass(frozen=True)
class CcStats:
    """Live CC summary statistics for an ICCS instance.

    `mean_selected`/`sem_selected` are computed from seismograms with
    `select=True` only; `mean_all`/`sem_all` from every seismogram. SEM
    fields are `None` when fewer than two values are available.

    Computed directly from `ICCS.ccs` against the current stack, so it is
    always up to date without a DB round-trip. For the persisted-quality
    equivalent (event/station/snapshot scoped, DB-backed), see
    `aimbat.models.SeismogramQualityStats`.
    """

    n_all: int
    mean_all: float | None
    sem_all: float | None
    n_selected: int
    mean_selected: float | None
    sem_selected: float | None

IccsLifecycle dataclass

Framework-agnostic state and policy for keeping a BoundICCS up to date.

Owns the bound instance plus the bookkeeping needed to create it asynchronously without blocking a UI thread: an in-progress guard so overlapping creation attempts collapse into one, a one-shot retry flag for recovering from a transient failure, and a fallback staleness check (event.stack_modified) for the window before any instance exists yet.

Callers own the actual I/O and threading (reading waveform data, running in a background thread, marshalling results back to a UI thread) and call the methods here only to record the outcome. See AimbatTUI's _IccsLifecycleMixin for the reference (Textual-based) caller.

Parameters:

Name Type Description Default
bound BoundICCS | None
None
retry_pending bool
False
stack_modified_seen Timestamp | None
None

Methods:

Name Description
assign

Record a newly created instance as ready.

clear

Discard the bound instance without arming a retry.

is_stale

Return True if the bound instance (or lack of one) needs rebuilding.

mark_aborted

Clear the in-progress flag without arming a retry.

mark_failed

Record a failed creation attempt.

note_checked

Record the stack_modified value observed at a staleness check.

start_creating

Begin a creation attempt, discarding any existing bound instance.

Attributes:

Name Type Description
ready bool

Whether a bound ICCS instance is currently available.

Source code in src/aimbat/core/_iccs.py
@dataclass
class IccsLifecycle:
    """Framework-agnostic state and policy for keeping a `BoundICCS` up to date.

    Owns the bound instance plus the bookkeeping needed to create it
    asynchronously without blocking a UI thread: an in-progress guard so
    overlapping creation attempts collapse into one, a one-shot retry flag
    for recovering from a transient failure, and a fallback staleness check
    (`event.stack_modified`) for the window before any instance exists yet.

    Callers own the actual I/O and threading (reading waveform data, running
    in a background thread, marshalling results back to a UI thread) and
    call the methods here only to record the outcome. See `AimbatTUI`'s
    `_IccsLifecycleMixin` for the reference (Textual-based) caller.
    """

    bound: BoundICCS | None = None
    retry_pending: bool = False
    stack_modified_seen: Timestamp | None = None
    _creating: bool = field(default=False, repr=False, init=False)

    @property
    def ready(self) -> bool:
        """Whether a bound ICCS instance is currently available."""
        return self.bound is not None

    def is_stale(self, event: AimbatEvent) -> bool:
        """Return True if the bound instance (or lack of one) needs rebuilding.

        Delegates to `BoundICCS.is_stale` when an instance exists; otherwise
        falls back to comparing `event.stack_modified` against the value last
        recorded via `note_checked`.

        Args:
            event: The event to check against.
        """
        if self.bound is not None:
            return self.bound.is_stale(event)
        return event.stack_modified != self.stack_modified_seen

    def start_creating(self) -> bool:
        """Begin a creation attempt, discarding any existing bound instance.

        Returns:
            False if a creation attempt is already in progress, in which
            case the caller should skip starting a new one.
        """
        if self._creating:
            return False
        self._creating = True
        self.bound = None
        self.retry_pending = False
        return True

    def mark_aborted(self) -> None:
        """Clear the in-progress flag without arming a retry.

        Use when creation could not even be attempted (e.g. no event
        selected) rather than when it was attempted and failed.
        """
        self._creating = False

    def mark_failed(self, *, is_retry: bool) -> None:
        """Record a failed creation attempt.

        Args:
            is_retry: Whether this attempt was itself the one-shot retry. A
                retry is not allowed to re-arm itself, so a persistently
                failing event gets exactly one automatic retry rather than
                retrying forever.
        """
        self._creating = False
        if not is_retry:
            self.retry_pending = True

    def assign(self, bound_iccs: BoundICCS) -> None:
        """Record a newly created instance as ready."""
        self._creating = False
        self.bound = bound_iccs

    def clear(self) -> None:
        """Discard the bound instance without arming a retry.

        Use when the instance is invalidated by something other than a
        failed creation attempt (e.g. its event was deleted).
        """
        self.bound = None

    def note_checked(self, stack_modified: Timestamp | None) -> None:
        """Record the `stack_modified` value observed at a staleness check.

        Only meaningful while `bound` is `None`; used by `is_stale` to
        detect further changes before a new instance can be created.
        """
        self.stack_modified_seen = stack_modified

ready property

ready: bool

Whether a bound ICCS instance is currently available.

assign

assign(bound_iccs: BoundICCS) -> None

Record a newly created instance as ready.

Source code in src/aimbat/core/_iccs.py
def assign(self, bound_iccs: BoundICCS) -> None:
    """Record a newly created instance as ready."""
    self._creating = False
    self.bound = bound_iccs

clear

clear() -> None

Discard the bound instance without arming a retry.

Use when the instance is invalidated by something other than a failed creation attempt (e.g. its event was deleted).

Source code in src/aimbat/core/_iccs.py
def clear(self) -> None:
    """Discard the bound instance without arming a retry.

    Use when the instance is invalidated by something other than a
    failed creation attempt (e.g. its event was deleted).
    """
    self.bound = None

is_stale

is_stale(event: AimbatEvent) -> bool

Return True if the bound instance (or lack of one) needs rebuilding.

Delegates to BoundICCS.is_stale when an instance exists; otherwise falls back to comparing event.stack_modified against the value last recorded via note_checked.

Parameters:

Name Type Description Default
event AimbatEvent

The event to check against.

required
Source code in src/aimbat/core/_iccs.py
def is_stale(self, event: AimbatEvent) -> bool:
    """Return True if the bound instance (or lack of one) needs rebuilding.

    Delegates to `BoundICCS.is_stale` when an instance exists; otherwise
    falls back to comparing `event.stack_modified` against the value last
    recorded via `note_checked`.

    Args:
        event: The event to check against.
    """
    if self.bound is not None:
        return self.bound.is_stale(event)
    return event.stack_modified != self.stack_modified_seen

mark_aborted

mark_aborted() -> None

Clear the in-progress flag without arming a retry.

Use when creation could not even be attempted (e.g. no event selected) rather than when it was attempted and failed.

Source code in src/aimbat/core/_iccs.py
def mark_aborted(self) -> None:
    """Clear the in-progress flag without arming a retry.

    Use when creation could not even be attempted (e.g. no event
    selected) rather than when it was attempted and failed.
    """
    self._creating = False

mark_failed

mark_failed(*, is_retry: bool) -> None

Record a failed creation attempt.

Parameters:

Name Type Description Default
is_retry bool

Whether this attempt was itself the one-shot retry. A retry is not allowed to re-arm itself, so a persistently failing event gets exactly one automatic retry rather than retrying forever.

required
Source code in src/aimbat/core/_iccs.py
def mark_failed(self, *, is_retry: bool) -> None:
    """Record a failed creation attempt.

    Args:
        is_retry: Whether this attempt was itself the one-shot retry. A
            retry is not allowed to re-arm itself, so a persistently
            failing event gets exactly one automatic retry rather than
            retrying forever.
    """
    self._creating = False
    if not is_retry:
        self.retry_pending = True

note_checked

note_checked(stack_modified: Timestamp | None) -> None

Record the stack_modified value observed at a staleness check.

Only meaningful while bound is None; used by is_stale to detect further changes before a new instance can be created.

Source code in src/aimbat/core/_iccs.py
def note_checked(self, stack_modified: Timestamp | None) -> None:
    """Record the `stack_modified` value observed at a staleness check.

    Only meaningful while `bound` is `None`; used by `is_stale` to
    detect further changes before a new instance can be created.
    """
    self.stack_modified_seen = stack_modified

start_creating

start_creating() -> bool

Begin a creation attempt, discarding any existing bound instance.

Returns:

Type Description
bool

False if a creation attempt is already in progress, in which

bool

case the caller should skip starting a new one.

Source code in src/aimbat/core/_iccs.py
def start_creating(self) -> bool:
    """Begin a creation attempt, discarding any existing bound instance.

    Returns:
        False if a creation attempt is already in progress, in which
        case the caller should skip starting a new one.
    """
    if self._creating:
        return False
    self._creating = True
    self.bound = None
    self.retry_pending = False
    return True

IccsValidationError

Bases: ValueError

Raised when an event's parameters cannot produce an ICCS instance.

A ValueError subclass, so callers that only care that the parameters were rejected can keep catching ValueError.

Source code in src/aimbat/core/_iccs.py
class IccsValidationError(ValueError):
    """Raised when an event's parameters cannot produce an ICCS instance.

    A `ValueError` subclass, so callers that only care that the parameters
    were rejected can keep catching `ValueError`.
    """

NoSeismogramsError

Bases: RuntimeError

Raised when an ICCS instance is requested for an event with no seismograms.

An event with no seismograms has no stack, so any downstream access to correlation values or the stack raises deep inside pysmo. Callers get this clear, catchable error up front instead.

Source code in src/aimbat/core/_iccs.py
class NoSeismogramsError(RuntimeError):
    """Raised when an ICCS instance is requested for an event with no seismograms.

    An event with no seismograms has no stack, so any downstream access to
    correlation values or the stack raises deep inside pysmo. Callers get
    this clear, catchable error up front instead.
    """

PruneReport dataclass

What prune_project found and (unless dry_run) did.

Parameters:

Name Type Description Default
orphan_seismograms list[AimbatSeismogram]

Seismograms whose data source probed absent.

required
orphan_sources dict[UUID, tuple[str, DataType]]

Seismogram ID -> (sourcename, datatype) of the vanished source.

required
unprobeable dict[DataType, int]

Datatype -> count of sources with no registered probe (reconcile incomplete).

required
emptied_stations list[AimbatStation]

Stations left with no seismograms once the orphans are gone.

required
emptied_events list[AimbatEvent]

Events left with no seismograms; pruned only if prune_empty_events.

required
events_needing_invalidation list[AimbatEvent]

Events that lose at least one seismogram and are not themselves pruned.

required
deleted_snapshots dict[UUID, int]

Event ID -> number of snapshots destroyed by pruning that event.

required
cascaded_notes dict[str, int]

"seismogram" | "station" | "event" -> count of notes cascade-deleted.

required
warnings list[str]

Non-fatal warnings to surface to the user.

required
dry_run bool
required
pruned bool

True once the deletions have been committed.

False

Attributes:

Name Type Description
cascaded_notes dict[str, int]

"seismogram" | "station" | "event" -> count of notes cascade-deleted.

deleted_snapshots dict[UUID, int]

Event ID -> number of snapshots destroyed by pruning that event.

emptied_events list[AimbatEvent]

Events left with no seismograms; pruned only if prune_empty_events.

emptied_stations list[AimbatStation]

Stations left with no seismograms once the orphans are gone.

events_needing_invalidation list[AimbatEvent]

Events that lose at least one seismogram and are not themselves pruned.

orphan_seismograms list[AimbatSeismogram]

Seismograms whose data source probed absent.

orphan_sources dict[UUID, tuple[str, DataType]]

Seismogram ID -> (sourcename, datatype) of the vanished source.

pruned bool

True once the deletions have been committed.

total_notes int

Total notes that will be, or were, cascade-deleted.

total_snapshots int

Total snapshots that will be, or were, destroyed.

unprobeable dict[DataType, int]

Datatype -> count of sources with no registered probe (reconcile incomplete).

warnings list[str]

Non-fatal warnings to surface to the user.

Source code in src/aimbat/core/_prune.py
@dataclass
class PruneReport:
    """What `prune_project` found and (unless `dry_run`) did."""

    orphan_seismograms: list[AimbatSeismogram]
    """Seismograms whose data source probed absent."""
    orphan_sources: dict[UUID, tuple[str, DataType]]
    """Seismogram ID -> (sourcename, datatype) of the vanished source."""
    unprobeable: dict[DataType, int]
    """Datatype -> count of sources with no registered probe (reconcile incomplete)."""
    emptied_stations: list[AimbatStation]
    """Stations left with no seismograms once the orphans are gone."""
    emptied_events: list[AimbatEvent]
    """Events left with no seismograms; pruned only if `prune_empty_events`."""
    events_needing_invalidation: list[AimbatEvent]
    """Events that lose at least one seismogram and are not themselves pruned."""
    deleted_snapshots: dict[UUID, int]
    """Event ID -> number of snapshots destroyed by pruning that event."""
    cascaded_notes: dict[str, int]
    """"seismogram" | "station" | "event" -> count of notes cascade-deleted."""
    warnings: list[str]
    """Non-fatal warnings to surface to the user."""
    dry_run: bool
    pruned: bool = field(default=False)
    """True once the deletions have been committed."""

    @property
    def total_snapshots(self) -> int:
        """Total snapshots that will be, or were, destroyed."""
        return sum(self.deleted_snapshots.values())

    @property
    def total_notes(self) -> int:
        """Total notes that will be, or were, cascade-deleted."""
        return sum(self.cascaded_notes.values())

cascaded_notes instance-attribute

cascaded_notes: dict[str, int]

"seismogram" | "station" | "event" -> count of notes cascade-deleted.

deleted_snapshots instance-attribute

deleted_snapshots: dict[UUID, int]

Event ID -> number of snapshots destroyed by pruning that event.

emptied_events instance-attribute

emptied_events: list[AimbatEvent]

Events left with no seismograms; pruned only if prune_empty_events.

emptied_stations instance-attribute

emptied_stations: list[AimbatStation]

Stations left with no seismograms once the orphans are gone.

events_needing_invalidation instance-attribute

events_needing_invalidation: list[AimbatEvent]

Events that lose at least one seismogram and are not themselves pruned.

orphan_seismograms instance-attribute

orphan_seismograms: list[AimbatSeismogram]

Seismograms whose data source probed absent.

orphan_sources instance-attribute

orphan_sources: dict[UUID, tuple[str, DataType]]

Seismogram ID -> (sourcename, datatype) of the vanished source.

pruned class-attribute instance-attribute

pruned: bool = field(default=False)

True once the deletions have been committed.

total_notes property

total_notes: int

Total notes that will be, or were, cascade-deleted.

total_snapshots property

total_snapshots: int

Total snapshots that will be, or were, destroyed.

unprobeable instance-attribute

unprobeable: dict[DataType, int]

Datatype -> count of sources with no registered probe (reconcile incomplete).

warnings instance-attribute

warnings: list[str]

Non-fatal warnings to surface to the user.

SchemaMismatchError

Bases: RuntimeError

An un-stamped database's live schema doesn't match a known revision.

Source code in src/aimbat/core/_migrations.py
class SchemaMismatchError(RuntimeError):
    """An un-stamped database's live schema doesn't match a known revision."""

SchemaStaleWarning

Bases: UserWarning

A project database's schema is behind the latest Alembic revision.

Raised via warnings.warn() rather than printed directly, so that library users can react to it programmatically - e.g. promote it to a hard error with warnings.simplefilter("error", SchemaStaleWarning) - without AIMBAT itself having to decide whether a stale schema should block execution. For CLI/subprocess scripting, use the AIMBAT_STRICT_SCHEMA_CHECK setting instead: PYTHONWARNINGS/-W do not reliably work for custom warning categories like this one, since Python resolves dotted category paths very early during interpreter startup, before the aimbat package is reliably importable (see aimbat.db's module docstring).

Attributes:

Name Type Description
current_revision

The revision the database is stamped at, or None for a pre-Alembic database with no version history at all.

head_revision

The latest revision available in AIMBAT's migration scripts.

Source code in src/aimbat/core/_migrations.py
class SchemaStaleWarning(UserWarning):
    """A project database's schema is behind the latest Alembic revision.

    Raised via `warnings.warn()` rather than printed directly, so that
    library users can react to it programmatically - e.g. promote it to a
    hard error with `warnings.simplefilter("error", SchemaStaleWarning)` -
    without AIMBAT itself having to decide whether a stale schema should
    block execution. For CLI/subprocess scripting, use the
    `AIMBAT_STRICT_SCHEMA_CHECK` setting instead: `PYTHONWARNINGS`/`-W` do
    *not* reliably work for custom warning categories like this one, since
    Python resolves dotted category paths very early during interpreter
    startup, before the `aimbat` package is reliably importable (see
    `aimbat.db`'s module docstring).

    Attributes:
        current_revision: The revision the database is stamped at, or `None`
            for a pre-Alembic database with no version history at all.
        head_revision: The latest revision available in AIMBAT's migration
            scripts.
    """

    def __init__(
        self, message: str, current_revision: str | None, head_revision: str | None
    ) -> None:
        super().__init__(message)
        self.current_revision = current_revision
        self.head_revision = head_revision

SyncResult

Bases: NamedTuple

Which live quality metrics sync_from_matching_hash repopulated.

resync_quality falls back to clear_mccc_quality when mccc_synced is False, so it reports whether the restore landed, not merely whether a matching snapshot was found.

Parameters:

Name Type Description Default
mccc_synced bool
None
iccs_synced bool
None
Source code in src/aimbat/core/_snapshot.py
class SyncResult(NamedTuple):
    """Which live quality metrics `sync_from_matching_hash` repopulated.

    `resync_quality` falls back to `clear_mccc_quality` when `mccc_synced` is
    `False`, so it reports whether the restore landed, not merely whether a
    matching snapshot was found.
    """

    mccc_synced: bool
    iccs_synced: bool

add_data_to_project

add_data_to_project(
    session: Session,
    data_sources: Sequence[PathLike[str] | str],
    data_type: DataType,
    station_id: UUID | None = None,
    event_id: UUID | None = None,
    dry_run: bool = False,
    on_progress: Callable[[int, int], None] | None = None,
) -> tuple[
    list[AimbatDataSource],
    set[UUID],
    set[UUID],
    set[UUID],
    list[str],
]

Add data sources to the AIMBAT database.

What gets created depends on which capabilities data_type supports:

  • Station + event + seismogram: all three records are created and linked, and an AimbatDataSource entry is stored.
  • Station or event only (e.g. JSON_STATION, JSON_EVENT): only the relevant metadata records are created; no seismogram or data source entry is stored.

Use station_id or event_id to skip extracting station or event metadata from the data source and link to a pre-existing record instead.

A new event whose origin time nearly, but not exactly, matches a pre-existing event's time is handled per settings.event_duplicate_tolerance, event_duplicate_raise_tolerance, and event_duplicate_strict. Within the tighter tolerance the existing event is reused, with a log warning (a dry run additionally returns the message in duplicate_warnings). Within the wider "ambiguous gap" band, a ValueError is raised unconditionally, even during a dry run.

Parameters:

Name Type Description Default
session Session

The SQLModel database session.

required
data_sources Sequence[PathLike[str] | str]

List of data sources to add.

required
data_type DataType

Type of data.

required
station_id UUID | None

UUID of an existing station to use instead of extracting one from each data source.

None
event_id UUID | None

UUID of an existing event to use instead of extracting one from each data source.

None
dry_run bool

If True, do not commit changes to the database.

False
on_progress Callable[[int, int], None] | None

Optional callback invoked as on_progress(done, total) after each data source is processed, for callers that want to display progress.

None

Returns:

Type Description
list[AimbatDataSource]

A 5-tuple of `(added_datasources, existing_station_ids,

set[UUID]

existing_event_ids, existing_seismogram_ids, duplicate_warnings)`.

set[UUID]

added_datasources is every AimbatDataSource touched by this call,

set[UUID]

new or reused. existing_*_ids are the sets of station/event/

list[str]

seismogram IDs that already existed in the database before this

tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]

call, so callers can tell which entries in added_datasources are

tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]

newly created versus reused by comparing IDs against these sets.

tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]

duplicate_warnings lists near-duplicate-event reuse messages

tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]

collected during a dry run (always empty on a real-add call, which

tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]

logs the same message as a warning instead).

Raises:

Type Description
ValueError

If a near-duplicate event falls in the "ambiguous gap" band (see above).

Anything raised while a data source is being processed carries a Data source: <name> note identifying it, which utils.exception_message renders alongside the message.

Source code in src/aimbat/core/_data.py
def add_data_to_project(
    session: Session,
    data_sources: Sequence[os.PathLike[str] | str],
    data_type: DataType,
    station_id: UUID | None = None,
    event_id: UUID | None = None,
    dry_run: bool = False,
    on_progress: Callable[[int, int], None] | None = None,
) -> tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]:
    """Add data sources to the AIMBAT database.

    What gets created depends on which capabilities `data_type` supports:

    - Station + event + seismogram: all three records are created and linked,
      and an `AimbatDataSource` entry is stored.
    - Station or event only (e.g. `JSON_STATION`, `JSON_EVENT`): only the
      relevant metadata records are created; no seismogram or data source entry
      is stored.

    Use `station_id` or `event_id` to skip extracting station or event metadata
    from the data source and link to a pre-existing record instead.

    A new event whose origin time nearly, but not exactly, matches a
    pre-existing event's time is handled per
    `settings.event_duplicate_tolerance`, `event_duplicate_raise_tolerance`,
    and `event_duplicate_strict`. Within the tighter tolerance the existing
    event is reused, with a log warning (a dry run additionally returns the
    message in `duplicate_warnings`). Within the wider "ambiguous gap" band,
    a `ValueError` is raised unconditionally, even during a dry run.

    Args:
        session: The SQLModel database session.
        data_sources: List of data sources to add.
        data_type: Type of data.
        station_id: UUID of an existing station to use instead of extracting
            one from each data source.
        event_id: UUID of an existing event to use instead of extracting one
            from each data source.
        dry_run: If True, do not commit changes to the database.
        on_progress: Optional callback invoked as `on_progress(done, total)`
            after each data source is processed, for callers that want to
            display progress.

    Returns:
        A 5-tuple of `(added_datasources, existing_station_ids,
        existing_event_ids, existing_seismogram_ids, duplicate_warnings)`.
        `added_datasources` is every `AimbatDataSource` touched by this call,
        new or reused. `existing_*_ids` are the sets of station/event/
        seismogram IDs that already existed in the database *before* this
        call, so callers can tell which entries in `added_datasources` are
        newly created versus reused by comparing IDs against these sets.
        `duplicate_warnings` lists near-duplicate-event reuse messages
        collected during a dry run (always empty on a real-add call, which
        logs the same message as a warning instead).

    Raises:
        ValueError: If a near-duplicate event falls in the "ambiguous gap"
            band (see above).

    Anything raised while a data source is being processed carries a
    `Data source: <name>` note identifying it, which
    `utils.exception_message` renders alongside the message.
    """

    logger.info(f"Adding {len(data_sources)} {data_type} data sources to project.")

    missing_sources = [str(ds) for ds in data_sources if not Path(ds).exists()]
    if missing_sources:
        raise FileNotFoundError(
            "Data source(s) not found: " + ", ".join(missing_sources)
        )

    if station_id is not None and session.get(AimbatStation, station_id) is None:
        raise NoResultFound(f"No station found with ID {station_id}.")
    if event_id is not None and session.get(AimbatEvent, event_id) is None:
        raise NoResultFound(f"No event found with ID {event_id}.")

    existing_station_ids, existing_event_ids, existing_seismogram_ids = (
        _existing_entity_ids(session)
    )

    # Mutated in place as new events are created, so near-duplicate
    # detection also covers events created earlier in this same batch,
    # unlike existing_event_ids, which stays a frozen pre-batch snapshot
    # for the return value's new-vs-reused semantics.
    known_event_ids = set(existing_event_ids)

    try:
        added_datasources: list[AimbatDataSource] = []
        duplicate_warnings: list[str] = []
        total = len(data_sources)
        with session.begin_nested() as nested:
            for done, datasource in enumerate(data_sources, start=1):
                try:
                    result, duplicate_warning = _process_datasource(
                        session,
                        datasource,
                        data_type,
                        station_id,
                        event_id,
                        dry_run,
                        known_event_ids,
                    )
                except Exception as e:
                    # A note rather than a wrapping exception, so callers
                    # catching the documented ValidationError/ValueError/
                    # NoResultFound still do. Displayed by
                    # `utils.exception_message`.
                    e.add_note(f"Data source: {datasource}")
                    raise
                if result is not None:
                    added_datasources.append(result)
                if duplicate_warning is not None:
                    duplicate_warnings.append(duplicate_warning)
                if on_progress is not None:
                    on_progress(done, total)

            if dry_run:
                _roll_back_dry_run(session, nested, added_datasources)
                return (
                    added_datasources,
                    existing_station_ids,
                    existing_event_ids,
                    existing_seismogram_ids,
                    duplicate_warnings,
                )

        session.commit()
        logger.info("Data added successfully.")
        return (
            added_datasources,
            existing_station_ids,
            existing_event_ids,
            existing_seismogram_ids,
            duplicate_warnings,
        )

    except Exception as e:
        logger.error(
            "Failed to add data. Rolling back changes. Error: " + exception_message(e)
        )
        raise

add_seismograms_to_project

add_seismograms_to_project(
    session: Session,
    items: Sequence[tuple[IccsSeismogram, Station, Event]],
    *,
    data_dir: str | PathLike[str],
    dry_run: bool = False,
    on_progress: Callable[[int, int], None] | None = None,
) -> tuple[
    list[AimbatDataSource],
    set[UUID],
    set[UUID],
    set[UUID],
    list[str],
]

Ingest already-built (seismogram, station, event) triples directly.

The general-purpose counterpart to add_data_to_project for a caller that already has pysmo Seismogram/Station/Event-shaped objects in memory - fetched from a web service, built in a notebook, or produced by a library such as pysmo.tools.project.PysmoProject - with no file round-trip through a DataType AIMBAT already understands. Each seismogram's waveform is persisted as a miniSEED file under data_dir so it can be read back later like any other data source; station and event metadata are taken directly from the objects given.

Every triple must carry a real Event: AimbatSeismogram.event_id is a required foreign key, and AIMBAT's processing model (ICCS, MCCC, snapshots, the active-event mechanism) is per-event throughout, so an event-less seismogram would have nothing to attach to. Reuse the same Event object across several triples to link them to one event - _link_event's near-duplicate detection still applies, so a second, slightly different Event object (e.g. from a re-run) resolves to the same row rather than duplicating it.

Parameters:

Name Type Description Default
session Session

The SQLModel database session.

required
items Sequence[tuple[IccsSeismogram, Station, Event]]

Sequence of (seismogram, station, event) triples to add.

required
data_dir str | PathLike[str]

Directory the persisted miniSEED files are written to. Created (with any missing parents) if it does not exist yet.

required
dry_run bool

If True, do not commit changes to the database.

False
on_progress Callable[[int, int], None] | None

Optional callback invoked as on_progress(done, total) after each triple is processed, for callers that want to display progress.

None

Returns:

Type Description
list[AimbatDataSource]

Same 5-tuple shape as add_data_to_project:

set[UUID]

`(added_datasources, existing_station_ids, existing_event_ids,

set[UUID]

existing_seismogram_ids, duplicate_warnings)`.

Raises:

Type Description
ValueError

If a near-duplicate event falls in the "ambiguous gap" band (see add_data_to_project).

Note

Miniseed files are written to data_dir before the final commit. If that commit fails (disk full, a constraint violation surfaced only at commit time, a concurrent writer), the files already written survive on disk with no corresponding database row - an orphan aimbat data prune cannot detect, since prune only looks for database rows whose file has vanished, never the reverse. Low severity (disk clutter, not data loss) and the failure window is narrow, but worth knowing about before relying on data_dir being an exact mirror of the database.

Source code in src/aimbat/core/_data.py
def add_seismograms_to_project(
    session: Session,
    items: Sequence[tuple[IccsSeismogram, Station, Event]],
    *,
    data_dir: str | os.PathLike[str],
    dry_run: bool = False,
    on_progress: Callable[[int, int], None] | None = None,
) -> tuple[list[AimbatDataSource], set[UUID], set[UUID], set[UUID], list[str]]:
    """Ingest already-built (seismogram, station, event) triples directly.

    The general-purpose counterpart to `add_data_to_project` for a caller
    that already has pysmo `Seismogram`/`Station`/`Event`-shaped objects in
    memory - fetched from a web service, built in a notebook, or produced by
    a library such as `pysmo.tools.project.PysmoProject` - with no file
    round-trip through a `DataType` AIMBAT already understands. Each
    seismogram's waveform is persisted as a miniSEED file under `data_dir`
    so it can be read back later like any other data source; station and
    event metadata are taken directly from the objects given.

    Every triple must carry a real `Event`: `AimbatSeismogram.event_id` is a
    required foreign key, and AIMBAT's processing model (ICCS, MCCC,
    snapshots, the active-event mechanism) is per-event throughout, so an
    event-less seismogram would have nothing to attach to. Reuse the same
    `Event` object across several triples to link them to one event -
    `_link_event`'s near-duplicate detection still applies, so a second,
    slightly different `Event` object (e.g. from a re-run) resolves to the
    same row rather than duplicating it.

    Args:
        session: The SQLModel database session.
        items: Sequence of `(seismogram, station, event)` triples to add.
        data_dir: Directory the persisted miniSEED files are written to.
            Created (with any missing parents) if it does not exist yet.
        dry_run: If True, do not commit changes to the database.
        on_progress: Optional callback invoked as `on_progress(done, total)`
            after each triple is processed, for callers that want to
            display progress.

    Returns:
        Same 5-tuple shape as `add_data_to_project`:
        `(added_datasources, existing_station_ids, existing_event_ids,
        existing_seismogram_ids, duplicate_warnings)`.

    Raises:
        ValueError: If a near-duplicate event falls in the "ambiguous gap"
            band (see `add_data_to_project`).

    Note:
        Miniseed files are written to `data_dir` before the final commit. If
        that commit fails (disk full, a constraint violation surfaced only
        at commit time, a concurrent writer), the files already written
        survive on disk with no corresponding database row - an orphan
        `aimbat data prune` cannot detect, since prune only looks for
        database rows whose file has vanished, never the reverse. Low
        severity (disk clutter, not data loss) and the failure window is
        narrow, but worth knowing about before relying on `data_dir` being
        an exact mirror of the database.
    """

    logger.info(f"Adding {len(items)} seismogram(s) to project directly.")

    data_dir = Path(data_dir)

    existing_station_ids, existing_event_ids, existing_seismogram_ids = (
        _existing_entity_ids(session)
    )

    known_event_ids = set(existing_event_ids)

    try:
        added_datasources: list[AimbatDataSource] = []
        duplicate_warnings: list[str] = []
        pending_writes: list[tuple[IccsSeismogram, Station, Path]] = []
        total = len(items)
        with session.begin_nested() as nested:
            for done, (seismogram, station, event) in enumerate(items, start=1):
                label = f"{station.network}.{station.name}"
                aimbat_station = _link_station(
                    session, AimbatStation.model_validate(station), label
                )

                new_aimbat_event = AimbatEvent.model_validate(
                    event, update={"parameters": AimbatEventParameters()}
                )
                aimbat_event, duplicate_warning = _link_event(
                    session, new_aimbat_event, dry_run, known_event_ids, label
                )
                if duplicate_warning is not None:
                    duplicate_warnings.append(duplicate_warning)

                path = _seismogram_path(seismogram, station, data_dir)
                sourcename, datatype = str(path), DataType.MSEED
                already_persisted = (
                    session.exec(
                        select(AimbatDataSource.id).where(
                            AimbatDataSource.sourcename == sourcename
                        )
                    ).first()
                    is not None
                )
                if already_persisted:
                    message = _format_seismogram_collision_message(label, sourcename)
                    logger.warning(message)
                    if dry_run:
                        duplicate_warnings.append(message)
                else:
                    # Writing is deferred until the whole batch has been
                    # validated and linked, so a later triple's failure (e.g.
                    # an ambiguous-gap event) never leaves earlier triples'
                    # files orphaned on disk with no committed DB row.
                    pending_writes.append((seismogram, station, path))

                new_aimbat_seismogram = AimbatSeismogram.model_validate(
                    seismogram,
                    update={
                        "t0": seismogram.t0,
                        "parameters": AimbatSeismogramParameters(),
                    },
                )
                aimbat_seismogram = _link_seismogram(
                    session, new_aimbat_seismogram, sourcename
                )
                aimbat_seismogram.station = aimbat_station
                aimbat_seismogram.event = aimbat_event

                logger.debug(
                    f"Linking seismogram from {sourcename} to Station="
                    + f"{aimbat_station.name} and EventTime={aimbat_event.time}."
                )

                aimbat_data_source = _link_datasource(
                    session, sourcename, datatype, aimbat_seismogram
                )
                added_datasources.append(aimbat_data_source)

                if on_progress is not None:
                    on_progress(done, total)

            if dry_run:
                _roll_back_dry_run(session, nested, added_datasources)
                return (
                    added_datasources,
                    existing_station_ids,
                    existing_event_ids,
                    existing_seismogram_ids,
                    duplicate_warnings,
                )

            if pending_writes:
                data_dir.mkdir(parents=True, exist_ok=True)
                written: list[Path] = []
                try:
                    for (
                        pending_seismogram,
                        pending_station,
                        pending_path,
                    ) in pending_writes:
                        _persist(pending_seismogram, pending_station, pending_path)
                        written.append(pending_path)
                except Exception:
                    for written_path in written:
                        written_path.unlink(missing_ok=True)
                    raise

        session.commit()
        logger.info("Data added successfully.")
        return (
            added_datasources,
            existing_station_ids,
            existing_event_ids,
            existing_seismogram_ids,
            duplicate_warnings,
        )

    except Exception as e:
        logger.error(f"Failed to add data. Rolling back changes. Error: {e}")
        raise

build_iccs_from_snapshot

build_iccs_from_snapshot(
    session: Session, snapshot_id: UUID
) -> BoundICCS

Build a read-only BoundICCS from a snapshot's parameters and live waveform data.

Uses the snapshot's event and seismogram parameters (window, t1, flip, select, bandpass, etc.) but reads waveform data from the live datasources. Seismograms added after the snapshot was taken are not included in the snapshot; their live parameters are used instead. No DB writes occur at any point.

Parameters:

Name Type Description Default
session Session

Database session.

required
snapshot_id UUID

ID of the AimbatSnapshot to load.

required

Returns:

Type Description
BoundICCS

BoundICCS instance built from the snapshot parameters.

Raises:

Type Description
ValueError

If no snapshot with the given ID is found.

Source code in src/aimbat/core/_iccs.py
def build_iccs_from_snapshot(session: Session, snapshot_id: UUID) -> BoundICCS:
    """Build a read-only BoundICCS from a snapshot's parameters and live waveform data.

    Uses the snapshot's event and seismogram parameters (window, t1, flip, select,
    bandpass, etc.) but reads waveform data from the live datasources. Seismograms
    added after the snapshot was taken are not included in the snapshot; their live
    parameters are used instead. No DB writes occur at any point.

    Args:
        session: Database session.
        snapshot_id: ID of the AimbatSnapshot to load.

    Returns:
        BoundICCS instance built from the snapshot parameters.

    Raises:
        ValueError: If no snapshot with the given ID is found.
    """
    logger.info(f"Building ICCS from snapshot {snapshot_id}.")

    statement = (
        select(AimbatSnapshot)
        .where(AimbatSnapshot.id == snapshot_id)
        .options(
            selectinload(rel(AimbatSnapshot.event))
            .selectinload(rel(AimbatEvent.seismograms))
            .selectinload(rel(AimbatSeismogram.parameters)),
            selectinload(rel(AimbatSnapshot.event_parameters_snapshot)),
            selectinload(rel(AimbatSnapshot.seismogram_parameters_snapshots)),
        )
    )
    snapshot = session.exec(statement).one_or_none()

    if snapshot is None:
        raise ValueError(f"Snapshot {snapshot_id} not found.")

    ep = snapshot.event_parameters_snapshot
    snap_params = AimbatEventParametersBase.model_validate(ep)

    # Build a map from seismogram_id → snapshot parameters
    snap_seis_map = {
        sp.seismogram_id: sp for sp in snapshot.seismogram_parameters_snapshots
    }

    seismograms = []
    for seis in snapshot.event.seismograms:
        snap_sp = snap_seis_map.get(seis.id)
        if snap_sp is None:
            # Seismogram was added after the snapshot: use live parameters
            seis_params = AimbatSeismogramParametersBase.model_validate(seis.parameters)
        else:
            seis_params = AimbatSeismogramParametersBase.model_validate(snap_sp)
        seismograms.append(
            MiniIccsSeismogram(
                begin_time=seis.begin_time,
                delta=seis.delta,
                data=seis.data,
                t0=seis.t0,
                t1=seis_params.t1,
                flip=seis_params.flip,
                select=seis_params.select,
                extra={"id": seis.id},
            )
        )

    iccs = ICCS(
        seismograms=seismograms,
        window_pre=snap_params.window_pre,
        window_post=snap_params.window_post,
        ramp_width=snap_params.ramp_width,
        bandpass_apply=snap_params.bandpass_apply,
        bandpass_fmin=snap_params.bandpass_fmin,
        bandpass_fmax=snap_params.bandpass_fmax,
        corners=snap_params.corners,
        min_cc=snap_params.min_cc,
        context_width=settings.context_width,
    )
    return BoundICCS(
        iccs=iccs,
        event_id=snapshot.event_id,
        created_at=Timestamp.now("UTC"),
    )

cc_stats

cc_stats(iccs: ICCS) -> CcStats

Summarise live CC values (mean ± SEM) across all and selected seismograms.

Uses ICCS.ccs, which correlates each seismogram against the current stack, so results are always up to date without requiring iccs() to have been called first.

Parameters:

Name Type Description Default
iccs ICCS

ICCS instance.

required

Returns:

Type Description
CcStats

CcStats summarising the live CC values.

Source code in src/aimbat/core/_iccs.py
def cc_stats(iccs: ICCS) -> CcStats:
    """Summarise live CC values (mean ± SEM) across all and selected seismograms.

    Uses `ICCS.ccs`, which correlates each seismogram against the current
    stack, so results are always up to date without requiring `iccs()` to
    have been called first.

    Args:
        iccs: ICCS instance.

    Returns:
        CcStats summarising the live CC values.
    """
    ccs = [float(cc) for cc in iccs.ccs]
    selected_ccs = [cc for cc, seis in zip(ccs, iccs.seismograms) if seis.select]
    mean_all, sem_all = mean_and_sem(ccs)
    mean_selected, sem_selected = mean_and_sem(selected_ccs)
    return CcStats(
        n_all=len(ccs),
        mean_all=mean_all,
        sem_all=sem_all,
        n_selected=len(selected_ccs),
        mean_selected=mean_selected,
        sem_selected=sem_selected,
    )

clear_iccs_cache

clear_iccs_cache() -> None

Clear the process-level ICCS cache.

Source code in src/aimbat/core/_iccs.py
def clear_iccs_cache() -> None:
    """Clear the process-level ICCS cache."""
    _iccs_cache.clear()

clear_mccc_quality

clear_mccc_quality(
    session: Session, event: AimbatEvent
) -> None

Clear MCCC quality metrics from the live quality tables for an event.

Sets all MCCC fields (mccc_rmse, mccc_error, mccc_cc_mean, mccc_cc_std) to None for the event and all its seismograms. ICCS CC values are not affected.

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent whose quality should be cleared.

required
Source code in src/aimbat/core/_iccs.py
def clear_mccc_quality(session: Session, event: AimbatEvent) -> None:
    """Clear MCCC quality metrics from the live quality tables for an event.

    Sets all MCCC fields (`mccc_rmse`, `mccc_error`, `mccc_cc_mean`,
    `mccc_cc_std`) to `None` for the event and all its seismograms.
    ICCS CC values are not affected.

    Args:
        session: Database session.
        event: AimbatEvent whose quality should be cleared.
    """
    logger.debug(f"Clearing MCCC quality for event {event.id}.")

    if event.quality is not None:
        event.quality.mccc_rmse = None
        session.add(event.quality)

    for seis in event.seismograms:
        if seis.quality is not None:
            seis.quality.mccc_error = None
            seis.quality.mccc_cc_mean = None
            seis.quality.mccc_cc_std = None
            session.add(seis.quality)

    session.commit()

compute_iccs_hash

compute_iccs_hash(event: AimbatEvent) -> str

Compute the hash identifying the ICCS stack these parameters produce.

Covers the window, taper and bandpass event parameters and the per-seismogram t1, flip and select flags. A match means a snapshot's iccs_cc values were measured against the same stack composition and can be restored.

Parameters:

Name Type Description Default
event AimbatEvent

AimbatEvent whose current parameters should be hashed.

required

Returns:

Type Description
str

Hex-encoded SHA-256 digest.

Source code in src/aimbat/core/_snapshot.py
def compute_iccs_hash(event: AimbatEvent) -> str:
    """Compute the hash identifying the ICCS stack these parameters produce.

    Covers the window, taper and bandpass event parameters and the
    per-seismogram `t1`, `flip` and `select` flags. A match means a snapshot's
    `iccs_cc` values were measured against the same stack composition and can
    be restored.

    Args:
        event: AimbatEvent whose current parameters should be hashed.

    Returns:
        Hex-encoded SHA-256 digest.
    """
    logger.debug(f"Computing ICCS parameters hash for event {event.id}.")
    return _compute_parameters_hash(
        event,
        event_exclude=_ICCS_HASH_EVENT_EXCLUDE,
        seismogram_exclude=set(),
    )

compute_mccc_hash

compute_mccc_hash(event: AimbatEvent) -> str

Compute the hash identifying the MCCC inversion these parameters produce.

Covers the window, taper and bandpass event parameters plus mccc_damp and mccc_min_cc, and the per-seismogram t1 and flip flags. select is excluded: MCCC run membership is recorded by the snapshot's seismogram quality rows, so a selection change does not invalidate a prior result.

Parameters:

Name Type Description Default
event AimbatEvent

AimbatEvent whose current parameters should be hashed.

required

Returns:

Type Description
str

Hex-encoded SHA-256 digest.

Source code in src/aimbat/core/_snapshot.py
def compute_mccc_hash(event: AimbatEvent) -> str:
    """Compute the hash identifying the MCCC inversion these parameters produce.

    Covers the window, taper and bandpass event parameters plus `mccc_damp`
    and `mccc_min_cc`, and the per-seismogram `t1` and `flip` flags. `select`
    is excluded: MCCC run membership is recorded by the snapshot's seismogram
    quality rows, so a selection change does not invalidate a prior result.

    Args:
        event: AimbatEvent whose current parameters should be hashed.

    Returns:
        Hex-encoded SHA-256 digest.
    """
    logger.debug(f"Computing MCCC parameters hash for event {event.id}.")
    return _compute_parameters_hash(
        event,
        event_exclude=_MCCC_HASH_EVENT_EXCLUDE,
        seismogram_exclude=_MCCC_HASH_SEISMOGRAM_EXCLUDE,
    )

create_iccs_instance

create_iccs_instance(
    session: Session, event: AimbatEvent
) -> BoundICCS

Return a BoundICCS instance for the given event.

Returns the cached instance when it is still fresh (i.e. event.stack_modified has not advanced since the instance was created). Otherwise builds a new one and updates the cache. Building one writes each seismogram's ICCS CC value to the live quality table on session, flushed but not committed: the caller decides whether those values are kept. If session rolls back first, the new instance is dropped from the cache again.

MiniIccsSeismogram instances are constructed directly from each AimbatSeismogram, passing data by reference to the read-only io cache. No waveform data are copied.

Only event.id is trusted from the passed event: on a cache miss the function re-selects it from session, eagerly loading parameters and seismograms. event must therefore already be attached to session.

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent.

required

Returns:

Type Description
BoundICCS

BoundICCS instance tied to the given event.

Source code in src/aimbat/core/_iccs.py
def create_iccs_instance(session: Session, event: AimbatEvent) -> BoundICCS:
    """Return a BoundICCS instance for the given event.

    Returns the cached instance when it is still fresh (i.e. `event.stack_modified`
    has not advanced since the instance was created). Otherwise builds a new one
    and updates the cache. Building one writes each seismogram's ICCS CC value to
    the live quality table on `session`, flushed but not committed: the caller
    decides whether those values are kept. If `session` rolls back first, the
    new instance is dropped from the cache again.

    `MiniIccsSeismogram` instances are constructed directly from each
    `AimbatSeismogram`, passing `data` by reference to the read-only io cache.
    No waveform data are copied.

    Only `event.id` is trusted from the passed `event`: on a cache miss the
    function re-selects it from `session`, eagerly loading `parameters` and
    `seismograms`. `event` must therefore already be attached to `session`.

    Args:
        session: Database session.
        event: AimbatEvent.

    Returns:
        BoundICCS instance tied to the given event.

    """
    cached = _iccs_cache.get(event.id)
    if cached is not None and not cached.is_stale(event):
        logger.debug(f"Returning cached BoundICCS for event {event.id}.")
        _iccs_cache.move_to_end(event.id)
        return cached

    # Stamp before reading the event's parameters and waveforms below, so a
    # modification committed while this instance is being built is caught as
    # stale on the next check rather than silently baked in.
    created_at = Timestamp.now("UTC")

    event = session.exec(
        select(AimbatEvent)
        .where(AimbatEvent.id == event.id)
        .options(
            selectinload(rel(AimbatEvent.parameters)),
            selectinload(rel(AimbatEvent.seismograms)).selectinload(
                rel(AimbatSeismogram.parameters)
            ),
        )
    ).one()

    if not event.seismograms:
        raise NoSeismogramsError(
            f"Event {event.id} has no seismograms; cannot build an ICCS instance."
        )

    logger.debug(f"Creating ICCS instance for event {event.id}.")
    bound = BoundICCS(
        iccs=_build_iccs(event),
        event_id=event.id,
        created_at=created_at,
    )
    # Cache only after the stats write succeeds: a half-initialised instance
    # left in the cache would be returned (without re-raising) on the next
    # call and blow up later on first stack access.
    _write_iccs_stats(session, event.id, bound.iccs)
    _iccs_cache[event.id] = bound
    _iccs_cache.move_to_end(event.id)
    _evict_on_rollback(session, bound)
    if len(_iccs_cache) > _ICCS_CACHE_MAX_ENTRIES:
        _iccs_cache.popitem(last=False)
    return bound

create_project

create_project(engine: Engine) -> None

Initialise a new AIMBAT project database schema and triggers.

Creates all tables from SQLModel.metadata, then, for SQLite engines, creates the triggers that track event modification times and automatically null quality metrics when the parameters they depend on change. The new database is stamped at the latest Alembic revision.

Tables, triggers and the stamp are written in one transaction, so a failure partway through leaves no half-made project behind: SQLite rolls schema changes back like any other statement. A database file may still be created on disk by the connection itself, but it will hold no AIMBAT tables, and create_project can simply be run again.

Parameters:

Name Type Description Default
engine Engine

The SQLAlchemy/SQLModel Engine instance connected to the target database.

required

Raises:

Type Description
RuntimeError

If a project schema already exists in the target database.

Source code in src/aimbat/core/_project.py
def create_project(engine: Engine) -> None:
    """Initialise a new AIMBAT project database schema and triggers.

    Creates all tables from `SQLModel.metadata`, then, for SQLite engines,
    creates the triggers that track event modification times and
    automatically null quality metrics when the parameters they depend on
    change. The new database is stamped at the latest Alembic revision.

    Tables, triggers and the stamp are written in one transaction, so a
    failure partway through leaves no half-made project behind: SQLite rolls
    schema changes back like any other statement. A database file may still
    be created on disk by the connection itself, but it will hold no AIMBAT
    tables, and `create_project` can simply be run again.

    Args:
        engine: The SQLAlchemy/SQLModel Engine instance connected to the target database.

    Raises:
        RuntimeError: If a project schema already exists in the target database.
    """

    # Import locally to ensure SQLModel registers all table metadata before create_all()
    import aimbat.models  # noqa: F401

    logger.info(f"Creating new project in {engine.url}.")

    if _project_exists(engine):
        raise RuntimeError(
            f"Unable to create a new project: project already exists at {engine.url}!"
        )

    logger.debug("Creating database tables and loading defaults.")

    # Import locally to break a circular import at module level.
    from aimbat.core._migrations import stamp_head

    with engine.connect().execution_options(isolation_level="AUTOCOMMIT") as connection:
        # SQLite can roll DDL back, but the driver commits before each DDL
        # statement unless the transaction is opened by hand - which would
        # leave a failure partway through this function half-applied, as a
        # database with tables but no quality invalidation, or no stamp.
        # AUTOCOMMIT hands transaction control over, so the BEGIN below
        # covers the tables, the triggers and the stamp together.
        connection.exec_driver_sql("BEGIN")
        try:
            SQLModel.metadata.create_all(connection)

            if engine.name == "sqlite":
                _create_triggers(connection)

            # Mark the new database as being at the latest Alembic revision so
            # that `aimbat db upgrade` treats it consistently with a database
            # that was brought up to date via a real migration, rather than as
            # an unversioned legacy database.
            stamp_head(connection)
        except Exception:
            connection.exec_driver_sql("ROLLBACK")
            raise

        connection.exec_driver_sql("COMMIT")

create_snapshot

create_snapshot(
    session: Session,
    event: AimbatEvent,
    comment: str | None = None,
    automatic: bool = False,
) -> None

Create a snapshot of the AIMBAT processing parameters and quality metrics.

Parameter snapshots are always created. Quality snapshots are created whenever the corresponding live quality record has at least one non-None field. Seismogram quality is omitted when all quality fields are None (e.g. before any ICCS or MCCC run).

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent.

required
comment str | None

Optional comment.

None
automatic bool

Whether this snapshot was created automatically, e.g. by data add, rather than explicitly by the user.

False
Source code in src/aimbat/core/_snapshot.py
def create_snapshot(
    session: Session,
    event: AimbatEvent,
    comment: str | None = None,
    automatic: bool = False,
) -> None:
    """Create a snapshot of the AIMBAT processing parameters and quality metrics.

    Parameter snapshots are always created. Quality snapshots are created
    whenever the corresponding live quality record has at least one non-None
    field. Seismogram quality is omitted when all quality fields are `None`
    (e.g. before any ICCS or MCCC run).

    Args:
        session: Database session.
        event: AimbatEvent.
        comment: Optional comment.
        automatic: Whether this snapshot was created automatically, e.g. by
            `data add`, rather than explicitly by the user.
    """

    logger.info(
        f"Creating snapshot for event {event.id}"
        + (f" with comment '{comment}'" if comment else "")
        + "."
    )

    event = session.exec(
        select(AimbatEvent)
        .where(AimbatEvent.id == event.id)
        .options(
            selectinload(rel(AimbatEvent.parameters)),
            selectinload(rel(AimbatEvent.quality)),
            selectinload(rel(AimbatEvent.seismograms)).options(
                selectinload(rel(AimbatSeismogram.parameters)),
                selectinload(rel(AimbatSeismogram.quality)),
            ),
        )
    ).one()

    event_parameters_snapshot = AimbatEventParametersSnapshot.model_validate(
        event.parameters,
        update={
            "id": uuid4(),  # we don't want to carry over the id from the input event parameters
            "parameters_id": event.parameters.id,
        },
    )
    logger.debug(
        f"Adding event parameters snapshot with id={event_parameters_snapshot.id} to snapshot."
    )

    seismogram_parameter_snapshots = []
    for aimbat_seismogram in event.seismograms:
        seismogram_parameter_snapshot = AimbatSeismogramParametersSnapshot.model_validate(
            aimbat_seismogram.parameters,
            update={
                "id": uuid4(),  # we don't want to carry over the id from the input seismogram parameters
                "seismogram_id": aimbat_seismogram.id,
                "seismogram_parameters_id": aimbat_seismogram.parameters.id,
            },
        )
        logger.debug(
            f"Adding seismogram parameters snapshot with id={seismogram_parameter_snapshot.id} to snapshot."
        )
        seismogram_parameter_snapshots.append(seismogram_parameter_snapshot)

    # Capture quality metrics from the live quality tables.
    event_quality_snap: AimbatEventQualitySnapshot | None = None
    seis_quality_snaps: list[AimbatSeismogramQualitySnapshot] = []

    if event.quality is not None and event.quality.mccc_rmse is not None:
        logger.debug("Capturing event quality snapshot from live quality table.")
        event_quality_snap = AimbatEventQualitySnapshot.model_validate(
            event.quality,
            update={
                "id": uuid4(),
                "event_quality_id": event.quality.id,
            },
        )

    for aimbat_seismogram in event.seismograms:
        sq = aimbat_seismogram.quality
        if sq is None:
            continue
        if any(
            v is not None
            for v in [sq.iccs_cc, sq.mccc_cc_mean, sq.mccc_cc_std, sq.mccc_error]
        ):
            logger.debug(
                f"Adding seismogram quality snapshot for seismogram {aimbat_seismogram.id}."
            )
            seis_quality_snaps.append(
                AimbatSeismogramQualitySnapshot.model_validate(
                    sq,
                    update={
                        "id": uuid4(),
                        "seismogram_id": aimbat_seismogram.id,
                        "seismogram_quality_id": sq.id,
                    },
                )
            )

    highest_sequence = session.exec(
        select(func.max(col(AimbatSnapshot.sequence))).where(
            col(AimbatSnapshot.event_id) == event.id
        )
    ).one()

    aimbat_snapshot = AimbatSnapshot(
        event=event,
        sequence=(highest_sequence or 0) + 1,
        event_parameters_snapshot=event_parameters_snapshot,
        seismogram_parameters_snapshots=seismogram_parameter_snapshots,
        event_quality_snapshot=event_quality_snap,
        seismogram_quality_snapshots=seis_quality_snaps,
        comment=comment,
        automatic=automatic,
        mccc_hash=compute_mccc_hash(event),
        iccs_hash=compute_iccs_hash(event),
    )
    session.add(aimbat_snapshot)
    session.commit()

create_snapshots_for_added_data

create_snapshots_for_added_data(
    session: Session,
    added_datasources: Sequence[AimbatDataSource],
    existing_seismogram_ids: set[UUID],
    *,
    comment: str | None = None,
) -> tuple[list[UUID], list[tuple[UUID, str]]]

Create one snapshot per event that received a newly created seismogram.

Shared by the CLI's data add and the TUI's import action, both of which call add_data_to_project/add_seismograms_to_project first and pass its return values straight through.

Parameters:

Name Type Description Default
session Session

Database session.

required
added_datasources Sequence[AimbatDataSource]

AimbatDataSource rows touched by the ingestion call, new or reused.

required
existing_seismogram_ids set[UUID]

Seismogram IDs that already existed before the ingestion call, so only events with a genuinely new seismogram are snapshotted.

required
comment str | None

Snapshot comment. Defaults to a per-event "Added N seismogram(s)".

None

Returns:

Type Description
list[UUID]

A 2-tuple of (snapshotted, failures): snapshotted is the list of

list[tuple[UUID, str]]

event IDs a snapshot was created for; failures is a list of

tuple[list[UUID], list[tuple[UUID, str]]]

(event_id, error message) for events whose snapshot failed. A

tuple[list[UUID], list[tuple[UUID, str]]]

failure is logged here; the caller decides how to surface it

tuple[list[UUID], list[tuple[UUID, str]]]

(e.g. print_warning on the CLI, a notification in the TUI).

Source code in src/aimbat/core/_snapshot.py
def create_snapshots_for_added_data(
    session: Session,
    added_datasources: Sequence[AimbatDataSource],
    existing_seismogram_ids: set[UUID],
    *,
    comment: str | None = None,
) -> tuple[list[UUID], list[tuple[UUID, str]]]:
    """Create one snapshot per event that received a newly created seismogram.

    Shared by the CLI's `data add` and the TUI's import action, both of which
    call `add_data_to_project`/`add_seismograms_to_project` first and pass
    its return values straight through.

    Args:
        session: Database session.
        added_datasources: `AimbatDataSource` rows touched by the ingestion
            call, new or reused.
        existing_seismogram_ids: Seismogram IDs that already existed before
            the ingestion call, so only events with a genuinely new
            seismogram are snapshotted.
        comment: Snapshot comment. Defaults to a per-event
            `"Added N seismogram(s)"`.

    Returns:
        A 2-tuple of `(snapshotted, failures)`: `snapshotted` is the list of
        event IDs a snapshot was created for; `failures` is a list of
        `(event_id, error message)` for events whose snapshot failed. A
        failure is logged here; the caller decides how to surface it
        (e.g. `print_warning` on the CLI, a notification in the TUI).
    """
    from collections import Counter

    new_seismogram_counts: Counter[UUID] = Counter(
        ds.seismogram.event_id
        for ds in added_datasources
        if ds.seismogram_id not in existing_seismogram_ids
    )

    snapshotted: list[UUID] = []
    failures: list[tuple[UUID, str]] = []

    for event_id, count in new_seismogram_counts.items():
        event = session.get(AimbatEvent, event_id)
        if event is None:
            continue
        event_comment = (
            comment or f"Added {count} seismogram{'' if count == 1 else 's'}"
        )
        try:
            create_snapshot(session, event, comment=event_comment, automatic=True)
            snapshotted.append(event_id)
        except Exception as e:
            # By this point the ingestion call has already committed the
            # seismogram data, so a snapshot failure must not be reported as
            # an ingestion failure - that would misleadingly suggest the data
            # itself wasn't added. Roll back and move on to the next event
            # rather than raising, so one event's snapshot failure (e.g. an
            # unexpected quality-data shape) can't suppress the baseline for
            # an unrelated event touched in the same call. A failed commit
            # leaves the session unusable until rolled back, which would
            # otherwise make every subsequent event's snapshot fail too.
            session.rollback()
            logger.warning(
                f"Failed to create automatic snapshot for event {event_id}: {e}"
            )
            failures.append((event_id, str(e)))

    return snapshotted, failures

delete_event

delete_event(session: Session, event_id: UUID) -> None

Delete an AimbatEvent from the database.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

Event ID.

required

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

Source code in src/aimbat/core/_event.py
def delete_event(session: Session, event_id: UUID) -> None:
    """Delete an AimbatEvent from the database.

    Args:
        session: Database session.
        event_id: Event ID.

    Raises:
        NoResultFound: If no event with the given ID is found.
    """

    logger.info(f"Deleting event {event_id}.")

    event = session.get(AimbatEvent, event_id)
    if event is None:
        raise NoResultFound(f"Unable to find event using id: {event_id}.")

    session.delete(event)
    session.commit()

    from ._iccs import evict_iccs_cache_entry

    evict_iccs_cache_entry(event_id)

delete_project

delete_project(engine: Engine) -> None

Delete the AIMBAT project.

For a file-based SQLite database, deletes the database file itself. For an in-memory database, this is a no-op beyond disposing the engine.

Parameters:

Name Type Description Default
engine Engine

The SQLAlchemy/SQLModel Engine instance connected to the target database.

required

Raises:

Type Description
RuntimeError

If no project exists at engine, or if engine's driver is not pysqlite.

Source code in src/aimbat/core/_project.py
def delete_project(engine: Engine) -> None:
    """Delete the AIMBAT project.

    For a file-based SQLite database, deletes the database file itself. For
    an in-memory database, this is a no-op beyond disposing the engine.

    Args:
        engine: The SQLAlchemy/SQLModel Engine instance connected to the
            target database.

    Raises:
        RuntimeError: If no project exists at `engine`, or if `engine`'s
            driver is not `pysqlite`.
    """

    logger.info(f"Deleting project at {engine.url}.")

    if not _project_exists(engine):
        raise RuntimeError("No project found to delete.")

    if engine.driver == "pysqlite":
        database = engine.url.database
        engine.dispose()
        if database == ":memory:":
            logger.info("Running database in memory, nothing to delete.")
            return
        elif database:
            project_path = Path(database)
            resolved = project_path.resolve()
            if resolved == Path(resolved.anchor) or resolved == Path.home():
                raise RuntimeError(
                    f"Refusing to delete suspicious project path: {resolved}."
                )
            logger.info(f"Deleting project file: {project_path}.")
            project_path.unlink()
            for suffix in ("-wal", "-shm"):
                project_path.with_name(project_path.name + suffix).unlink(
                    missing_ok=True
                )
            return

    raise RuntimeError(
        f"Unable to delete project: unsupported engine driver '{engine.driver}'."
    )

delete_seismogram

delete_seismogram(
    session: Session, seismogram_id: UUID
) -> None

Delete an AimbatSeismogram from the database.

Parameters:

Name Type Description Default
session Session

Database session.

required
seismogram_id UUID

Seismogram ID.

required

Raises:

Type Description
NoResultFound

If no seismogram with the given ID is found.

Source code in src/aimbat/core/_seismogram.py
def delete_seismogram(session: Session, seismogram_id: UUID) -> None:
    """Delete an AimbatSeismogram from the database.

    Args:
        session: Database session.
        seismogram_id: Seismogram ID.

    Raises:
        NoResultFound: If no seismogram with the given ID is found.
    """

    logger.info(f"Deleting seismogram {seismogram_id}.")

    seismogram = session.get(AimbatSeismogram, seismogram_id)
    if seismogram is None:
        raise NoResultFound(f"No AimbatSeismogram found with {seismogram_id=}")

    from ._iccs import _invalidate_event_quality

    event_id = seismogram.event_id
    # No AFTER DELETE trigger fires, so the surviving seismograms' iccs_cc and
    # the event RMSE would otherwise be left computed against a stack that
    # still counted this one. Invalidate while every row is persistent: doing
    # it after the delete flush can hit a preloaded event collection that
    # still holds this seismogram and try to re-add its cascade-deleted
    # quality child.
    _invalidate_event_quality(session, event_id)
    session.delete(seismogram)
    session.commit()

delete_snapshot

delete_snapshot(
    session: Session, snapshot_id: UUID
) -> None

Delete an AIMBAT parameter snapshot.

Parameters:

Name Type Description Default
session Session

Database session.

required
snapshot_id UUID

Snapshot id.

required

Raises:

Type Description
NoResultFound

If no snapshot with the given ID is found.

Source code in src/aimbat/core/_snapshot.py
def delete_snapshot(session: Session, snapshot_id: UUID) -> None:
    """Delete an AIMBAT parameter snapshot.

    Args:
        session: Database session.
        snapshot_id: Snapshot id.

    Raises:
        NoResultFound: If no snapshot with the given ID is found.
    """
    logger.info(f"Deleting snapshot {snapshot_id}.")

    snapshot = session.get(AimbatSnapshot, snapshot_id)
    if snapshot is None:
        raise NoResultFound(f"Unable to find snapshot with {snapshot_id=}")

    session.delete(snapshot)
    session.commit()

delete_station

delete_station(session: Session, station_id: UUID) -> None

Delete an AimbatStation from the database.

Parameters:

Name Type Description Default
session Session

Database session.

required
station_id UUID

ID of the station to delete.

required

Raises:

Type Description
NoResultFound

If no station with the given ID is found.

Source code in src/aimbat/core/_station.py
def delete_station(session: Session, station_id: UUID) -> None:
    """Delete an AimbatStation from the database.

    Args:
        session: Database session.
        station_id: ID of the station to delete.

    Raises:
        NoResultFound: If no station with the given ID is found.
    """

    logger.info(f"Deleting station with id={station_id}.")

    station = session.get(AimbatStation, station_id)
    if station is None:
        raise NoResultFound(f"No AimbatStation found with {station_id=}")

    from ._iccs import _invalidate_event_quality

    # Deleting a station cascades to its seismograms across every event it
    # recorded; none of those events' quality is invalidated by a trigger.
    # Invalidate before deleting, while every seismogram row is still
    # persistent: a post-delete pass can hit a preloaded event collection
    # that still holds a cascade-deleted seismogram and its quality child.
    affected_event_ids = {seis.event_id for seis in station.seismograms}
    for event_id in affected_event_ids:
        _invalidate_event_quality(session, event_id)
    session.delete(station)
    session.commit()

dump_data_table

dump_data_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Return AIMBAT datasources table as a JSON-serialisable list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

UUID of the event to filter data sources by. If None, all data sources are returned.

None
by_alias bool

Whether to use field aliases.

False
by_title bool

Whether to use field titles (from the Pydantic model) for the field names in the output. Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

Aimbat datasources table as a list of dicts.

Source code in src/aimbat/core/_data.py
def dump_data_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Return AIMBAT datasources table as a JSON-serialisable list of dicts.

    Args:
        session: Database session.
        event_id: UUID of the event to filter data sources by. If None, all data sources are returned.
        by_alias: Whether to use field aliases.
        by_title: Whether to use field titles (from the Pydantic model) for the
            field names in the output. Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.

    Returns:
        Aimbat datasources table as a list of dicts.
    """
    logger.debug("Dumping AIMBAT datasources table to json.")

    check_field_name_flags(by_alias, by_title)

    if event_id is not None:
        data_source = get_data_for_event(session, event_id)
    else:
        data_source = session.exec(select(AimbatDataSource)).all()

    return dump_models(
        data_source,
        AimbatDataSource,
        by_alias=by_alias,
        by_title=by_title,
        exclude=exclude,
    )

dump_event_parameter_snapshot_table

dump_event_parameter_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump event parameter snapshots as a list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are dumped).

None
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts, one per event parameters snapshot.

Source code in src/aimbat/core/_snapshot.py
def dump_event_parameter_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump event parameter snapshots as a list of dicts.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided,
            snapshots for all events are dumped).
        by_alias: Whether to use serialisation aliases for the field names in the output.
        exclude: Set of field names to exclude from the output.

    Returns:
        List of dicts, one per event parameters snapshot.
    """
    return _dump_snapshot_related_table(
        session,
        "AimbatEventParametersSnapshot",
        AimbatEventParametersSnapshot,
        lambda s: [s.event_parameters_snapshot],
        event_id=event_id,
        by_alias=by_alias,
        exclude=exclude,
    )

dump_event_parameter_table

dump_event_parameter_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump the event parameter table data to json.

Parameters:

Name Type Description Default
session Session

Database session.

required
by_alias bool

Whether to use serialisation aliases for the field names.

False
by_title bool

Whether to use the field title metadata for the field names. Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter parameters by (if none is provided, parameters for all events are dumped).

None

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

Source code in src/aimbat/core/_event.py
def dump_event_parameter_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump the event parameter table data to json.

    Args:
        session: Database session.
        by_alias: Whether to use serialisation aliases for the field names.
        by_title: Whether to use the field title metadata for the field names.
            Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter parameters by (if none is provided,
            parameters for all events are dumped).

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
    """

    logger.debug("Dumping AIMBAT event parameter table to json.")

    check_field_name_flags(by_alias, by_title)

    if event_id is not None:
        statement = select(AimbatEventParameters).where(
            AimbatEventParameters.event_id == event_id
        )
    else:
        statement = select(AimbatEventParameters)

    parameters = session.exec(statement).all()

    return dump_models(
        parameters,
        AimbatEventParameters,
        by_alias=by_alias,
        by_title=by_title,
        exclude=exclude,
    )

dump_event_quality_snapshot_table

dump_event_quality_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump event quality snapshots as a list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are dumped).

None
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts, one per snapshot that has an event quality record

list[dict[str, Any]]

(snapshots taken before any quality data existed are omitted).

Source code in src/aimbat/core/_snapshot.py
def dump_event_quality_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump event quality snapshots as a list of dicts.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided,
            snapshots for all events are dumped).
        by_alias: Whether to use serialisation aliases for the field names in the output.
        exclude: Set of field names to exclude from the output.

    Returns:
        List of dicts, one per snapshot that has an event quality record
        (snapshots taken before any quality data existed are omitted).
    """
    return _dump_snapshot_related_table(
        session,
        "AimbatEventQualitySnapshot",
        AimbatEventQualitySnapshot,
        lambda s: (
            [s.event_quality_snapshot] if s.event_quality_snapshot is not None else []
        ),
        event_id=event_id,
        by_alias=by_alias,
        exclude=exclude,
    )

dump_event_quality_table

dump_event_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump event quality statistics to json.

Parameters:

Name Type Description Default
session Session

Database session.

required
by_alias bool

Whether to use serialisation aliases for the field names.

False
by_title bool

Whether to use the field title metadata for the field names. Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter by (if none is provided, quality for all events is dumped).

None

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

Source code in src/aimbat/core/_event.py
def dump_event_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump event quality statistics to json.

    Args:
        session: Database session.
        by_alias: Whether to use serialisation aliases for the field names.
        by_title: Whether to use the field title metadata for the field names.
            Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter by (if none is provided, quality for all
            events is dumped).

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
    """

    logger.debug("Dumping AIMBAT event quality table to json.")

    check_field_name_flags(by_alias, by_title)

    statement = select(AimbatEvent).options(
        selectinload(rel(AimbatEvent.seismograms)).selectinload(
            rel(AimbatSeismogram.quality)
        ),
        selectinload(rel(AimbatEvent.quality)),
    )
    if event_id is not None:
        statement = statement.where(AimbatEvent.id == event_id)

    events = session.exec(statement).all()
    stats = [SeismogramQualityStats.from_event(e) for e in events]

    return dump_models(
        stats,
        SeismogramQualityStats,
        by_alias=by_alias,
        by_title=by_title,
        exclude=(exclude or set()) | {"station_id", "snapshot_id"},
    )

dump_event_table

dump_event_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump the table data to a JSON-serialisable list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
from_read_model bool

Whether to dump from the read model (True) or the ORM model.

False
by_alias bool

Whether to use serialisation aliases for the field names.

False
by_title bool

Whether to use the field title metadata for the field names in the output (only applicable when from_read_model is True). Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

A list of dicts, one per event.

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

ValueError

If by_title is True but from_read_model is False.

Source code in src/aimbat/core/_event.py
def dump_event_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump the table data to a JSON-serialisable list of dicts.

    Args:
        session: Database session.
        from_read_model: Whether to dump from the read model (True) or the ORM model.
        by_alias: Whether to use serialisation aliases for the field names.
        by_title: Whether to use the field title metadata for the field names in the
            output (only applicable when from_read_model is True). Mutually
            exclusive with by_alias.
        exclude: Set of field names to exclude from the output.

    Returns:
        A list of dicts, one per event.

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
        ValueError: If `by_title` is True but `from_read_model` is False.
    """
    logger.debug("Dumping AIMBAT event table to json.")

    check_field_name_flags(by_alias, by_title, from_read_model)

    statement = select(AimbatEvent).options(
        selectinload(rel(AimbatEvent.seismograms)).selectinload(
            rel(AimbatSeismogram.parameters)
        ),
        selectinload(rel(AimbatEvent.parameters)),
        selectinload(rel(AimbatEvent.quality)),
    )
    if from_read_model:
        statement = statement.options(*undefer_counts(AimbatEvent))
    events = session.exec(statement).all()

    if from_read_model:
        event_reads = [AimbatEventRead.from_event(e, session=session) for e in events]
        return dump_models(
            event_reads,
            AimbatEventRead,
            by_alias=by_alias,
            by_title=by_title,
            exclude=exclude,
        )

    return dump_models(events, AimbatEvent, by_alias=by_alias, exclude=exclude)

dump_seismogram_parameter_snapshot_table

dump_seismogram_parameter_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump seismogram parameter snapshots as a list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are dumped).

None
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts, one per seismogram parameters snapshot across all

list[dict[str, Any]]

matching snapshots.

Source code in src/aimbat/core/_snapshot.py
def dump_seismogram_parameter_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump seismogram parameter snapshots as a list of dicts.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided,
            snapshots for all events are dumped).
        by_alias: Whether to use serialisation aliases for the field names in the output.
        exclude: Set of field names to exclude from the output.

    Returns:
        List of dicts, one per seismogram parameters snapshot across all
        matching snapshots.
    """
    return _dump_snapshot_related_table(
        session,
        "AimbatSeismogramParametersSnapshot",
        AimbatSeismogramParametersSnapshot,
        lambda s: s.seismogram_parameters_snapshots,
        event_id=event_id,
        by_alias=by_alias,
        exclude=exclude,
    )

dump_seismogram_parameter_table

dump_seismogram_parameter_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump the seismogram parameter table data to json serialisable list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
by_title bool

Whether to use titles for the field names in the output.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter seismogram parameters by (if none is provided, all seismogram parameters for all events are dumped).

None

Returns:

Type Description
list[dict[str, Any]]

list of dicts representing the seismogram parameters.

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

Source code in src/aimbat/core/_seismogram.py
def dump_seismogram_parameter_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump the seismogram parameter table data to json serialisable list of dicts.

    Args:
        session: Database session.
        by_alias: Whether to use serialisation aliases for the field names in the output.
        by_title: Whether to use titles for the field names in the output.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter seismogram parameters by (if none is provided,
            all seismogram parameters for all events are dumped).

    Returns:
        list of dicts representing the seismogram parameters.

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
    """
    logger.debug("Dumping AimbatSeismogramParameters table to json.")

    check_field_name_flags(by_alias, by_title)

    if event_id is not None:
        statement = (
            select(AimbatSeismogramParameters)
            .join(AimbatSeismogram)
            .where(AimbatSeismogram.event_id == event_id)
        )
    else:
        statement = select(AimbatSeismogramParameters)

    parameters = session.exec(statement).all()

    return dump_models(
        parameters,
        AimbatSeismogramParameters,
        by_alias=by_alias,
        by_title=by_title,
        exclude=exclude,
    )

dump_seismogram_quality_snapshot_table

dump_seismogram_quality_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump seismogram quality snapshots as a list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are dumped).

None
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts, one per seismogram quality record captured across all

list[dict[str, Any]]

matching snapshots.

Source code in src/aimbat/core/_snapshot.py
def dump_seismogram_quality_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    by_alias: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump seismogram quality snapshots as a list of dicts.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided,
            snapshots for all events are dumped).
        by_alias: Whether to use serialisation aliases for the field names in the output.
        exclude: Set of field names to exclude from the output.

    Returns:
        List of dicts, one per seismogram quality record captured across all
        matching snapshots.
    """
    return _dump_snapshot_related_table(
        session,
        "AimbatSeismogramQualitySnapshot",
        AimbatSeismogramQualitySnapshot,
        lambda s: s.seismogram_quality_snapshots,
        event_id=event_id,
        by_alias=by_alias,
        exclude=exclude,
    )

dump_seismogram_table

dump_seismogram_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump the AimbatSeismogram table to json serialisable list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
from_read_model bool

Whether to dump from the read model (True) or the ORM model.

False
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
by_title bool

Whether to use titles for the field names in the output (only applicable when from_read_model is True). Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter seismograms by (if none is provided, seismograms for all events are dumped).

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts representing the seismograms, built from the read

list[dict[str, Any]]

model when from_read_model is True or from the ORM model otherwise.

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

ValueError

If by_title is True but from_read_model is False.

Source code in src/aimbat/core/_seismogram.py
def dump_seismogram_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump the AimbatSeismogram table to json serialisable list of dicts.

    Args:
        session: Database session.
        from_read_model: Whether to dump from the read model (True) or the ORM model.
        by_alias: Whether to use serialisation aliases for the field names in the output.
        by_title: Whether to use titles for the field names in the output (only
            applicable when from_read_model is True). Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter seismograms by (if none is provided,
            seismograms for all events are dumped).

    Returns:
        List of dicts representing the seismograms, built from the read
        model when `from_read_model` is True or from the ORM model otherwise.

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
        ValueError: If `by_title` is True but `from_read_model` is False.
    """
    logger.debug("Dumping AIMBAT seismogram table to json.")

    check_field_name_flags(by_alias, by_title, from_read_model)

    if event_id is not None:
        statement = select(AimbatSeismogram).where(
            AimbatSeismogram.event_id == event_id
        )
    else:
        statement = select(AimbatSeismogram)

    statement = statement.options(
        selectinload(rel(AimbatSeismogram.station)),
        selectinload(rel(AimbatSeismogram.event)),
        selectinload(rel(AimbatSeismogram.parameters)),
        selectinload(rel(AimbatSeismogram.quality)),
    )

    seismograms = session.exec(statement).all()

    if from_read_model:
        seismogram_reads = [
            AimbatSeismogramRead.from_seismogram(s, session=session)
            for s in seismograms
        ]
        return dump_models(
            seismogram_reads,
            AimbatSeismogramRead,
            by_alias=by_alias,
            by_title=by_title,
            exclude=exclude,
        )

    return dump_models(
        seismograms, AimbatSeismogram, by_alias=by_alias, exclude=exclude
    )

dump_snapshot_quality_table

dump_snapshot_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump snapshot quality statistics to json.

Parameters:

Name Type Description Default
session Session

Database session.

required
by_alias bool

Whether to use serialisation aliases for the field names.

False
by_title bool

Whether to use the field title metadata for the field names. Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter snapshots by (if none is provided, quality for all snapshots is dumped).

None

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

Source code in src/aimbat/core/_snapshot.py
def dump_snapshot_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump snapshot quality statistics to json.

    Args:
        session: Database session.
        by_alias: Whether to use serialisation aliases for the field names.
        by_title: Whether to use the field title metadata for the field names.
            Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter snapshots by (if none is provided, quality
            for all snapshots is dumped).

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
    """

    logger.debug("Dumping AIMBAT snapshot quality table to json.")

    check_field_name_flags(by_alias, by_title)

    snapshots = get_snapshots(session, event_id, with_counts=True)
    stats = [SeismogramQualityStats.from_snapshot(s) for s in snapshots]

    return dump_models(
        stats,
        SeismogramQualityStats,
        by_alias=by_alias,
        by_title=by_title,
        exclude=(exclude or set()) | {"station_id"},
    )

dump_snapshot_results

dump_snapshot_results(
    session: Session,
    snapshot_id: UUID,
    by_alias: bool = False,
) -> dict[str, Any]

Dump per-seismogram MCCC results from a snapshot as a results envelope.

Returns a dict with event- and snapshot-level header fields plus a seismograms list containing one entry per seismogram. Event-level scalars (snapshot_id, event_id, mccc_rmse) appear once in the envelope rather than being repeated on every row.

Parameters:

Name Type Description Default
session Session

Database session.

required
snapshot_id UUID

UUID of the snapshot to export results from.

required
by_alias bool

Whether to use camelCase serialisation aliases for field names.

False

Returns:

Type Description
dict[str, Any]

Dict with header fields and a seismograms list.

Raises:

Type Description
NoResultFound

If no snapshot with the given ID is found.

Source code in src/aimbat/core/_snapshot.py
def dump_snapshot_results(
    session: Session,
    snapshot_id: UUID,
    by_alias: bool = False,
) -> dict[str, Any]:
    """Dump per-seismogram MCCC results from a snapshot as a results envelope.

    Returns a dict with event- and snapshot-level header fields plus a
    `seismograms` list containing one entry per seismogram. Event-level
    scalars (`snapshot_id`, `event_id`, `mccc_rmse`) appear once in the
    envelope rather than being repeated on every row.

    Args:
        session: Database session.
        snapshot_id: UUID of the snapshot to export results from.
        by_alias: Whether to use camelCase serialisation aliases for field names.

    Returns:
        Dict with header fields and a `seismograms` list.

    Raises:
        NoResultFound: If no snapshot with the given ID is found.
    """
    logger.debug(f"Dumping per-seismogram results for snapshot {snapshot_id}.")

    snapshot = session.exec(
        select(AimbatSnapshot)
        .where(AimbatSnapshot.id == snapshot_id)
        .options(
            selectinload(rel(AimbatSnapshot.event)),
            selectinload(rel(AimbatSnapshot.event_quality_snapshot)),
            selectinload(rel(AimbatSnapshot.seismogram_parameters_snapshots)),
            selectinload(rel(AimbatSnapshot.seismogram_quality_snapshots)),
        )
    ).one_or_none()

    if snapshot is None:
        raise NoResultFound(f"No AimbatSnapshot found with id: {snapshot_id}.")

    eq = snapshot.event_quality_snapshot
    mccc_rmse = eq.mccc_rmse if eq is not None else None

    # Resolve the frozen seismograms against the live table by their stored id;
    # some may have been deleted since the snapshot was taken.
    frozen_ids = {
        ps.seismogram_id
        for ps in snapshot.seismogram_parameters_snapshots
        if ps.seismogram_id is not None
    }
    live_seismograms: dict[UUID, AimbatSeismogram] = {}
    if frozen_ids:
        live_seismograms = {
            s.id: s
            for s in session.exec(
                select(AimbatSeismogram)
                .where(col(AimbatSeismogram.id).in_(frozen_ids))
                .options(selectinload(rel(AimbatSeismogram.station)))
            ).all()
        }

    quality_map: dict[UUID, AimbatSeismogramQualitySnapshot] = {
        sq.seismogram_id: sq
        for sq in snapshot.seismogram_quality_snapshots
        if sq.seismogram_id is not None
    }

    seismograms = [
        SnapshotSeismogramResult.from_snapshot_records(
            param_snap=ps,
            quality_snap=(
                quality_map.get(ps.seismogram_id)
                if ps.seismogram_id is not None
                else None
            ),
            live_seismogram=(
                live_seismograms.get(ps.seismogram_id)
                if ps.seismogram_id is not None
                else None
            ),
        )
        for ps in snapshot.seismogram_parameters_snapshots
    ]

    event = snapshot.event
    results = SnapshotResults(
        snapshot_id=snapshot.id,
        snapshot_time=snapshot.time,
        snapshot_comment=snapshot.comment,
        event_id=snapshot.event_id,
        event_time=event.time,
        event_latitude=event.latitude,
        event_longitude=event.longitude,
        event_depth=event.depth,
        mccc_rmse=mccc_rmse,
        seismograms=seismograms,
    )

    return results.model_dump(mode="json", by_alias=by_alias)

dump_snapshot_table

dump_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]

Dump snapshot metadata as a list of dicts.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are dumped).

None
from_read_model bool

Whether to dump from the read model (True) or the ORM model. Only affects the snapshots table.

False
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
by_title bool

Whether to use titles for the field names in the output (only applicable when from_read_model is True). Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts representing the snapshots, built from the read model

list[dict[str, Any]]

when from_read_model is True or from the ORM model otherwise.

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

ValueError

If by_title is True but from_read_model is False.

Source code in src/aimbat/core/_snapshot.py
def dump_snapshot_table(
    session: Session,
    event_id: UUID | None = None,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
) -> list[dict[str, Any]]:
    """Dump snapshot metadata as a list of dicts.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided,
            snapshots for all events are dumped).
        from_read_model: Whether to dump from the read model (True) or the ORM model.
            Only affects the `snapshots` table.
        by_alias: Whether to use serialisation aliases for the field names in the output.
        by_title: Whether to use titles for the field names in the output (only
            applicable when from_read_model is True). Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.

    Returns:
        List of dicts representing the snapshots, built from the read model
        when `from_read_model` is True or from the ORM model otherwise.

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
        ValueError: If `by_title` is True but `from_read_model` is False.
    """
    logger.debug("Dumping AimbatSnapshot table to json.")

    check_field_name_flags(by_alias, by_title, from_read_model)

    snapshots = get_snapshots(session, event_id, with_counts=from_read_model)

    if from_read_model:
        snapshots_read = [
            AimbatSnapshotRead.from_snapshot(s, session=session) for s in snapshots
        ]
        return dump_models(
            snapshots_read,
            AimbatSnapshotRead,
            by_alias=by_alias,
            by_title=by_title,
            exclude=exclude,
        )

    return dump_models(snapshots, AimbatSnapshot, by_alias=by_alias, exclude=exclude)

dump_station_quality_table

dump_station_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    station_id: UUID | None = None,
) -> list[dict[str, Any]]

Dump station quality statistics to json.

Parameters:

Name Type Description Default
session Session

Database session.

required
by_alias bool

Whether to use serialisation aliases for the field names.

False
by_title bool

Whether to use the field title metadata for the field names. Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
station_id UUID | None

Station ID to filter by (if none is provided, quality for all stations is dumped).

None

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

Source code in src/aimbat/core/_station.py
def dump_station_quality_table(
    session: Session,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    station_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Dump station quality statistics to json.

    Args:
        session: Database session.
        by_alias: Whether to use serialisation aliases for the field names.
        by_title: Whether to use the field title metadata for the field names.
            Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        station_id: Station ID to filter by (if none is provided, quality for
            all stations is dumped).

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
    """

    logger.debug("Dumping AIMBAT station quality table to json.")

    check_field_name_flags(by_alias, by_title)

    statement = select(AimbatStation).options(
        selectinload(rel(AimbatStation.seismograms)).selectinload(
            rel(AimbatSeismogram.quality)
        ),
    )
    if station_id is not None:
        statement = statement.where(AimbatStation.id == station_id)

    stations = session.exec(statement).all()
    stats = [SeismogramQualityStats.from_station(s) for s in stations]

    return dump_models(
        stats,
        SeismogramQualityStats,
        by_alias=by_alias,
        by_title=by_title,
        exclude=(exclude or set()) | {"event_id", "snapshot_id"},
    )

dump_station_table

dump_station_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]

Create a JSON serialisable dict from the AimbatStation table data.

Parameters:

Name Type Description Default
session Session

Database session.

required
from_read_model bool

Whether to dump from the read model (True) or the ORM model.

False
by_alias bool

Whether to use serialisation aliases for the field names in the output.

False
by_title bool

Whether to use titles for the field names in the output (only applicable when from_read_model is True). Mutually exclusive with by_alias.

False
exclude set[str] | None

Set of field names to exclude from the output.

None
event_id UUID | None

Event ID to filter seismograms by (if none is provided, seismograms for all events are dumped).

None

Returns:

Type Description
list[dict[str, Any]]

List of dicts representing the stations, built from the read model

list[dict[str, Any]]

when from_read_model is True or from the ORM model otherwise.

Raises:

Type Description
ValueError

If both by_alias and by_title are True.

ValueError

If by_title is True but from_read_model is False.

Source code in src/aimbat/core/_station.py
def dump_station_table(
    session: Session,
    from_read_model: bool = False,
    by_alias: bool = False,
    by_title: bool = False,
    exclude: set[str] | None = None,
    event_id: UUID | None = None,
) -> list[dict[str, Any]]:
    """Create a JSON serialisable dict from the AimbatStation table data.

    Args:
        session: Database session.
        from_read_model: Whether to dump from the read model (True) or the ORM model.
        by_alias: Whether to use serialisation aliases for the field names in the output.
        by_title: Whether to use titles for the field names in the output (only
            applicable when from_read_model is True). Mutually exclusive with by_alias.
        exclude: Set of field names to exclude from the output.
        event_id: Event ID to filter seismograms by (if none is provided,
            seismograms for all events are dumped).

    Returns:
        List of dicts representing the stations, built from the read model
        when `from_read_model` is True or from the ORM model otherwise.

    Raises:
        ValueError: If both `by_alias` and `by_title` are True.
        ValueError: If `by_title` is True but `from_read_model` is False.
    """

    check_field_name_flags(by_alias, by_title, from_read_model)

    logger.debug("Dumping AIMBAT station table to json.")

    if event_id is not None:
        statement = (
            select(AimbatStation)
            .join(AimbatSeismogram)
            .where(AimbatSeismogram.event_id == event_id)
            .distinct()
        )
    else:
        statement = select(AimbatStation)

    statement = statement.options(
        selectinload(rel(AimbatStation.seismograms)).selectinload(
            rel(AimbatSeismogram.quality)
        ),
        selectinload(rel(AimbatStation.seismograms)).selectinload(
            rel(AimbatSeismogram.parameters)
        ),
        selectinload(rel(AimbatStation.seismograms)).selectinload(
            rel(AimbatSeismogram.event)
        ),
        # Only the read model reads the counts.
        *(undefer_counts(AimbatStation) if from_read_model else ()),
    )

    stations = session.exec(statement).all()

    if from_read_model:
        read_stations = [
            AimbatStationRead.from_station(
                station=s,
                session=session,
            )
            for s in stations
        ]
        return dump_models(
            read_stations,
            AimbatStationRead,
            by_alias=by_alias,
            by_title=by_title,
            exclude=exclude,
        )

    return dump_models(stations, AimbatStation, by_alias=by_alias, exclude=exclude)

evict_iccs_cache_entry

evict_iccs_cache_entry(event_id: UUID) -> None

Remove one event's cached ICCS instance, if present.

Call this whenever an event is deleted, so its cached waveform data don't linger in memory for the rest of the process on the strength of an ID that no longer refers to anything.

Parameters:

Name Type Description Default
event_id UUID

ID of the deleted event.

required
Source code in src/aimbat/core/_iccs.py
def evict_iccs_cache_entry(event_id: UUID) -> None:
    """Remove one event's cached ICCS instance, if present.

    Call this whenever an event is deleted, so its cached waveform data
    don't linger in memory for the rest of the process on the strength of
    an ID that no longer refers to anything.

    Args:
        event_id: ID of the deleted event.
    """
    _iccs_cache.pop(event_id, None)

get_completed_events

get_completed_events(
    session: Session,
) -> Sequence[AimbatEvent]

Get the events marked as completed.

Parameters:

Name Type Description Default
session Session

SQL session.

required

Returns:

Type Description
Sequence[AimbatEvent]

All events where the completed parameter is set.

Source code in src/aimbat/core/_event.py
def get_completed_events(session: Session) -> Sequence[AimbatEvent]:
    """Get the events marked as completed.

    Args:
        session: SQL session.

    Returns:
        All events where the `completed` parameter is set.
    """

    logger.debug("Getting completed events from project.")

    statement = (
        select(AimbatEvent)
        .join(AimbatEventParameters)
        .where(col(AimbatEventParameters.completed).is_(True))
    )

    return session.exec(statement).all()

get_current_revision

get_current_revision(engine: Engine) -> str | None

Return the Alembic revision engine's database is stamped at.

Parameters:

Name Type Description Default
engine Engine

The SQLAlchemy/SQLModel Engine instance connected to the target database.

required

Returns:

Type Description
str | None

The current revision ID, or None if the database has never been

str | None

stamped (e.g. a pre-Alembic database, or one that doesn't exist yet).

Source code in src/aimbat/core/_migrations.py
def get_current_revision(engine: Engine) -> str | None:
    """Return the Alembic revision `engine`'s database is stamped at.

    Args:
        engine: The SQLAlchemy/SQLModel Engine instance connected to the
            target database.

    Returns:
        The current revision ID, or `None` if the database has never been
        stamped (e.g. a pre-Alembic database, or one that doesn't exist yet).
    """
    from alembic.runtime.migration import MigrationContext

    with engine.connect() as connection:
        migration_context = MigrationContext.configure(connection)
        heads = migration_context.get_current_heads()
    return heads[0] if heads else None

get_data_for_event

get_data_for_event(
    session: Session, event_id: UUID
) -> Sequence[AimbatDataSource]

Return the data sources belonging to the given event.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

UUID of the AimbatEvent.

required

Returns:

Type Description
Sequence[AimbatDataSource]

Sequence of AimbatDataSource objects belonging to the event.

Source code in src/aimbat/core/_data.py
def get_data_for_event(session: Session, event_id: UUID) -> Sequence[AimbatDataSource]:
    """Return the data sources belonging to the given event.

    Args:
        session: Database session.
        event_id: UUID of the AimbatEvent.

    Returns:
        Sequence of AimbatDataSource objects belonging to the event.
    """

    logger.debug(f"Getting data sources for event {event_id}.")

    statement = (
        select(AimbatDataSource)
        .join(AimbatSeismogram)
        .where(AimbatSeismogram.event_id == event_id)
    )
    return session.exec(statement).all()

get_event_quality

get_event_quality(
    session: Session, event_id: UUID
) -> SeismogramQualityStats

Get aggregated quality statistics for an event.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

UUID of the event.

required

Returns:

Type Description
SeismogramQualityStats

Aggregated seismogram quality statistics. The mccc_rmse field is

SeismogramQualityStats

taken from the event-level quality record rather than the per-seismogram

SeismogramQualityStats

records, and is None if MCCC has not been run.

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

Source code in src/aimbat/core/_event.py
def get_event_quality(session: Session, event_id: UUID) -> SeismogramQualityStats:
    """Get aggregated quality statistics for an event.

    Args:
        session: Database session.
        event_id: UUID of the event.

    Returns:
        Aggregated seismogram quality statistics. The `mccc_rmse` field is
        taken from the event-level quality record rather than the per-seismogram
        records, and is `None` if MCCC has not been run.

    Raises:
        NoResultFound: If no event with the given ID is found.
    """
    logger.debug(f"Getting quality stats for event {event_id}.")

    event = session.exec(
        select(AimbatEvent)
        .where(AimbatEvent.id == event_id)
        .options(
            selectinload(rel(AimbatEvent.seismograms)).selectinload(
                rel(AimbatSeismogram.quality)
            ),
            selectinload(rel(AimbatEvent.quality)),
        )
    ).one_or_none()

    if event is None:
        raise NoResultFound(f"No AimbatEvent found with id: {event_id}.")

    return SeismogramQualityStats.from_event(event)

get_events_using_station

get_events_using_station(
    session: Session, station_id: UUID
) -> Sequence[AimbatEvent]

Get all events that use a particular station.

Parameters:

Name Type Description Default
session Session

Database session.

required
station_id UUID

UUID of the station to return events for.

required

Returns:

Type Description
Sequence[AimbatEvent]

Events that use the station.

Source code in src/aimbat/core/_event.py
def get_events_using_station(
    session: Session, station_id: UUID
) -> Sequence[AimbatEvent]:
    """Get all events that use a particular station.

    Args:
        session: Database session.
        station_id: UUID of the station to return events for.

    Returns:
        Events that use the station.
    """

    logger.debug(f"Getting events for station: {station_id}.")

    statement = (
        select(AimbatEvent)
        .join(AimbatSeismogram)
        .join(AimbatStation)
        .where(AimbatStation.id == station_id)
        .options(
            selectinload(rel(AimbatEvent.seismograms)).selectinload(
                rel(AimbatSeismogram.parameters)
            ),
            selectinload(rel(AimbatEvent.parameters)),
            selectinload(rel(AimbatEvent.quality)),
        )
    )

    events = session.exec(statement).all()

    logger.debug(f"Found {len(events)}.")

    return events

get_head_revision

get_head_revision() -> str | None

Return the latest revision ID available in AIMBAT's migration scripts.

Reads _migrations/versions/ on disk only, so it needs no database connection and is safe to call from contexts where opening one would be unsafe (e.g. from inside a SQLAlchemy connect event, before the connection being established has been returned to the pool).

Returns:

Type Description
str | None

The head revision ID. None only if there are no migrations at all.

Source code in src/aimbat/core/_migrations.py
def get_head_revision() -> str | None:
    """Return the latest revision ID available in AIMBAT's migration scripts.

    Reads `_migrations/versions/` on disk only, so it needs no database
    connection and is safe to call from contexts where opening one would
    be unsafe (e.g. from inside a SQLAlchemy `connect` event, before the
    connection being established has been returned to the pool).

    Returns:
        The head revision ID. `None` only if there are no migrations at all.
    """
    from alembic.script import ScriptDirectory

    script_dir = ScriptDirectory(str(_migrations_dir()))
    return script_dir.get_current_head()

get_note_content

get_note_content(
    session: Session, target: NoteTarget, target_id: UUID
) -> str

Return the note content for the given entity.

Parameters:

Name Type Description Default
session Session

Active database session.

required
target NoteTarget

Entity type, one of event, station, seismogram, snapshot.

required
target_id UUID

UUID of the target entity.

required

Returns:

Type Description
str

Markdown note content, or an empty string if no note exists yet.

Source code in src/aimbat/core/_note.py
def get_note_content(session: Session, target: NoteTarget, target_id: uuid.UUID) -> str:
    """Return the note content for the given entity.

    Args:
        session: Active database session.
        target: Entity type, one of `event`, `station`, `seismogram`, `snapshot`.
        target_id: UUID of the target entity.

    Returns:
        Markdown note content, or an empty string if no note exists yet.
    """
    attr = _note_id_attr(target)
    note = session.exec(select(AimbatNote).where(attr == target_id)).one_or_none()
    return note.content if note is not None else ""

get_selected_seismograms

get_selected_seismograms(
    session: Session,
    event_id: UUID | None = None,
    all_events: bool = False,
) -> Sequence[AimbatSeismogram]

Get the selected seismograms for the given event.

A selected seismogram is one whose select parameter is set, i.e. one that is included in the ICCS stack and, by default, in MCCC.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to get seismograms for (only used when all_events is False).

None
all_events bool

Get the selected seismograms for all events.

False

Returns:

Type Description
Sequence[AimbatSeismogram]

Selected seismograms.

Raises:

Type Description
ValueError

If all_events is False and event_id is not given.

Source code in src/aimbat/core/_seismogram.py
def get_selected_seismograms(
    session: Session, event_id: UUID | None = None, all_events: bool = False
) -> Sequence[AimbatSeismogram]:
    """Get the selected seismograms for the given event.

    A selected seismogram is one whose `select` parameter is set, i.e. one
    that is included in the ICCS stack and, by default, in MCCC.

    Args:
        session: Database session.
        event_id: Event ID to get seismograms for (only used when all_events is False).
        all_events: Get the selected seismograms for all events.

    Returns:
        Selected seismograms.

    Raises:
        ValueError: If `all_events` is False and `event_id` is not given.
    """

    if all_events is True:
        logger.debug("Selecting seismograms for all events.")
        statement = (
            select(AimbatSeismogram)
            .join(AimbatSeismogramParameters)
            .where(col(AimbatSeismogramParameters.select).is_(True))
        )
    else:
        if event_id is None:
            raise ValueError("An event must be provided when all_events is False.")
        logger.debug(f"Selecting seismograms for event {event_id} only.")
        statement = (
            select(AimbatSeismogram)
            .join(AimbatSeismogramParameters)
            .where(col(AimbatSeismogramParameters.select).is_(True))
            .where(AimbatSeismogram.event_id == event_id)
        )

    statement = statement.options(
        selectinload(rel(AimbatSeismogram.station)),
        selectinload(rel(AimbatSeismogram.event)),
        selectinload(rel(AimbatSeismogram.parameters)),
        selectinload(rel(AimbatSeismogram.quality)),
    )
    seismograms = session.exec(statement).all()

    logger.debug(f"Found {len(seismograms)} selected seismograms.")

    return seismograms

get_snapshot_quality

get_snapshot_quality(
    session: Session, snapshot_id: UUID
) -> SeismogramQualityStats

Get aggregated quality statistics for a snapshot.

Parameters:

Name Type Description Default
session Session

Database session.

required
snapshot_id UUID

UUID of the snapshot.

required

Returns:

Type Description
SeismogramQualityStats

Aggregated seismogram quality statistics from the frozen snapshot records.

Raises:

Type Description
NoResultFound

If no snapshot with the given ID is found.

Source code in src/aimbat/core/_snapshot.py
def get_snapshot_quality(session: Session, snapshot_id: UUID) -> SeismogramQualityStats:
    """Get aggregated quality statistics for a snapshot.

    Args:
        session: Database session.
        snapshot_id: UUID of the snapshot.

    Returns:
        Aggregated seismogram quality statistics from the frozen snapshot records.

    Raises:
        NoResultFound: If no snapshot with the given ID is found.
    """
    logger.debug(f"Getting quality stats for snapshot {snapshot_id}.")

    snapshot = session.exec(
        select(AimbatSnapshot)
        .where(AimbatSnapshot.id == snapshot_id)
        .options(
            selectinload(rel(AimbatSnapshot.seismogram_quality_snapshots)),
            selectinload(rel(AimbatSnapshot.event_quality_snapshot)),
        )
    ).one_or_none()

    if snapshot is None:
        raise NoResultFound(f"No AimbatSnapshot found with id: {snapshot_id}.")

    return SeismogramQualityStats.from_snapshot(snapshot)

get_snapshots

get_snapshots(
    session: Session,
    event_id: UUID | None = None,
    with_counts: bool = False,
) -> Sequence[AimbatSnapshot]

Get the snapshots, optional filtered by event ID.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | None

Event ID to filter snapshots by (if none is provided, snapshots for all events are returned).

None
with_counts bool

Whether to load the deferred seismogram counts in the same query. Only the table-rendering paths read them; the hash-matching paths do not, and pay three scalar subqueries per row if they are.

False

Returns:

Type Description
Sequence[AimbatSnapshot]

Snapshots.

Source code in src/aimbat/core/_snapshot.py
def get_snapshots(
    session: Session, event_id: UUID | None = None, with_counts: bool = False
) -> Sequence[AimbatSnapshot]:
    """Get the snapshots, optional filtered by event ID.

    Args:
        session: Database session.
        event_id: Event ID to filter snapshots by (if none is provided, snapshots for all events are returned).
        with_counts: Whether to load the deferred seismogram counts in the same
            query. Only the table-rendering paths read them; the hash-matching
            paths do not, and pay three scalar subqueries per row if they are.

    Returns:
        Snapshots.
    """
    logger.debug("Getting AIMBAT snapshots.")

    if event_id is None:
        statement = select(AimbatSnapshot)
    else:
        statement = select(AimbatSnapshot).where(AimbatSnapshot.event_id == event_id)

    statement = statement.order_by(
        col(AimbatSnapshot.event_id), col(AimbatSnapshot.sequence)
    ).options(
        selectinload(rel(AimbatSnapshot.event)),
        selectinload(rel(AimbatSnapshot.event_parameters_snapshot)),
        selectinload(rel(AimbatSnapshot.seismogram_parameters_snapshots)),
        selectinload(rel(AimbatSnapshot.event_quality_snapshot)),
        selectinload(rel(AimbatSnapshot.seismogram_quality_snapshots)),
        *(undefer_counts(AimbatSnapshot) if with_counts else ()),
    )

    logger.debug(f"Executing statement to get snapshots: {statement}")
    return session.exec(statement).all()

get_station_iccs_ccs

get_station_iccs_ccs(
    session: Session, station_id: UUID
) -> tuple[float | None, ...]

Get ICCS cross-correlation coefficients for all seismograms of a station across all events.

Parameters:

Name Type Description Default
session Session

Database session.

required
station_id UUID

ID of the station.

required

Returns:

Type Description
tuple[float | None, ...]

Tuple of ICCS CC values, one per seismogram (None if not yet computed).

Source code in src/aimbat/core/_station.py
def get_station_iccs_ccs(
    session: Session, station_id: UUID
) -> tuple[float | None, ...]:
    """Get ICCS cross-correlation coefficients for all seismograms of a station across all events.

    Args:
        session: Database session.
        station_id: ID of the station.

    Returns:
        Tuple of ICCS CC values, one per seismogram (None if not yet computed).
    """
    logger.debug(f"Getting ICCS CCs for {station_id=}.")

    statement = (
        select(AimbatSeismogramQuality.iccs_cc)
        .join(AimbatSeismogram)
        .where(AimbatSeismogram.station_id == station_id)
    )

    return tuple(session.exec(statement).all())

get_station_quality

get_station_quality(
    session: Session, station_id: UUID
) -> SeismogramQualityStats

Get aggregated quality statistics for a station.

Parameters:

Name Type Description Default
session Session

Database session.

required
station_id UUID

UUID of the station.

required

Returns:

Type Description
SeismogramQualityStats

Aggregated seismogram quality statistics.

Raises:

Type Description
NoResultFound

If no station with the given ID is found.

Source code in src/aimbat/core/_station.py
def get_station_quality(session: Session, station_id: UUID) -> SeismogramQualityStats:
    """Get aggregated quality statistics for a station.

    Args:
        session: Database session.
        station_id: UUID of the station.

    Returns:
        Aggregated seismogram quality statistics.

    Raises:
        NoResultFound: If no station with the given ID is found.
    """
    logger.debug(f"Getting quality stats for station {station_id}.")

    station = session.exec(
        select(AimbatStation)
        .where(AimbatStation.id == station_id)
        .options(
            selectinload(rel(AimbatStation.seismograms)).selectinload(
                rel(AimbatSeismogram.quality)
            ),
        )
    ).one_or_none()

    if station is None:
        raise NoResultFound(f"No AimbatStation found with id: {station_id}.")

    return SeismogramQualityStats.from_station(station)

get_stations_in_event

get_stations_in_event(
    session: Session, event_id: UUID, as_json: bool = False
) -> Sequence[AimbatStation] | list[dict[str, Any]]

Get the stations for a particular event.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

ID of the event to get stations for.

required
as_json bool

Whether to return the result as JSON.

False

Returns:

Type Description
Sequence[AimbatStation] | list[dict[str, Any]]

Stations in the event, as ORM model instances or as a JSON-serialisable

Sequence[AimbatStation] | list[dict[str, Any]]

list of dicts depending on as_json.

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

Source code in src/aimbat/core/_station.py
def get_stations_in_event(
    session: Session, event_id: UUID, as_json: bool = False
) -> Sequence[AimbatStation] | list[dict[str, Any]]:
    """Get the stations for a particular event.

    Args:
        session: Database session.
        event_id: ID of the event to get stations for.
        as_json: Whether to return the result as JSON.

    Returns:
        Stations in the event, as ORM model instances or as a JSON-serialisable
        list of dicts depending on `as_json`.

    Raises:
        NoResultFound: If no event with the given ID is found.
    """
    logger.debug(f"Getting stations for event: {event_id}.")

    event = session.get(AimbatEvent, event_id)
    if event is None:
        raise NoResultFound(f"Unable to find event with {event_id=}")

    statement = (
        select(AimbatStation)
        .distinct()
        .join(AimbatSeismogram)
        .where(AimbatSeismogram.event_id == event.id)
        .options(
            selectinload(rel(AimbatStation.seismograms)).selectinload(
                rel(AimbatSeismogram.parameters)
            ),
            selectinload(rel(AimbatStation.seismograms)).selectinload(
                rel(AimbatSeismogram.quality)
            ),
            selectinload(rel(AimbatStation.seismograms)).selectinload(
                rel(AimbatSeismogram.event)
            ),
            # Only the read model reads the counts.
            *(undefer_counts(AimbatStation) if as_json else ()),
        )
    )

    logger.debug(f"Executing query: {statement}")
    results = session.exec(statement).all()

    if not as_json:
        return results

    read_stations = [
        AimbatStationRead.from_station(station=s, session=session) for s in results
    ]

    return dump_models(read_stations, AimbatStationRead)

prune_project

prune_project(
    session: Session,
    event_id: UUID | Literal["all"],
    *,
    dry_run: bool = False,
    prune_empty_stations: bool = False,
    prune_empty_events: bool = False,
    auto_snapshot: bool = True,
    force: bool = False,
    confirm: Callable[[PruneReport], bool] | None = None,
) -> PruneReport

Delete seismograms whose data source can no longer be read.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID | Literal['all']

A single event's ID, or the literal "all" for the whole project. Mirrors aimbat data list.

required
dry_run bool

Probe and report only; delete nothing.

False
prune_empty_stations bool

Also delete stations left with no seismograms.

False
prune_empty_events bool

Also delete events left with no seismograms. Deleting an event irreversibly destroys its snapshot history.

False
auto_snapshot bool

Snapshot every affected event before deleting anything. A snapshot failure aborts the prune with nothing deleted.

True
force bool

Treat a source whose presence cannot be determined (an unreadable file, a missing parent directory, an unreachable mount) as vanished instead of stopping. Without it, such a source is a hard error and nothing is pruned.

False
confirm Callable[[PruneReport], bool] | None

Optional callback invoked once with the completed PruneReport after probing but before any deletion (skipped on a dry_run or when nothing is orphaned). Return False to abort with nothing deleted; the same probe results are then used for the deletion, so the user acts on exactly what was reported.

None

Returns:

Type Description
PruneReport

A PruneReport.

Raises:

Type Description
NoResultFound

If event_id is not "all" and names no event.

SourceUnavailableError

If a source's presence cannot be determined and force is not set; nothing is pruned.

RuntimeError

If auto_snapshot is set and an affected event cannot be snapshotted; nothing is pruned.

Source code in src/aimbat/core/_prune.py
def prune_project(
    session: Session,
    event_id: UUID | Literal["all"],
    *,
    dry_run: bool = False,
    prune_empty_stations: bool = False,
    prune_empty_events: bool = False,
    auto_snapshot: bool = True,
    force: bool = False,
    confirm: Callable[[PruneReport], bool] | None = None,
) -> PruneReport:
    """Delete seismograms whose data source can no longer be read.

    Args:
        session: Database session.
        event_id: A single event's ID, or the literal `"all"` for the whole
            project. Mirrors `aimbat data list`.
        dry_run: Probe and report only; delete nothing.
        prune_empty_stations: Also delete stations left with no seismograms.
        prune_empty_events: Also delete events left with no seismograms.
            Deleting an event irreversibly destroys its snapshot history.
        auto_snapshot: Snapshot every affected event before deleting anything.
            A snapshot failure aborts the prune with nothing deleted.
        force: Treat a source whose presence cannot be determined (an
            unreadable file, a missing parent directory, an unreachable mount)
            as vanished instead of stopping. Without it, such a source is a
            hard error and nothing is pruned.
        confirm: Optional callback invoked once with the completed
            `PruneReport` after probing but before any deletion (skipped on a
            `dry_run` or when nothing is orphaned). Return `False` to abort
            with nothing deleted; the same probe results are then used for the
            deletion, so the user acts on exactly what was reported.

    Returns:
        A `PruneReport`.

    Raises:
        NoResultFound: If `event_id` is not `"all"` and names no event.
        SourceUnavailableError: If a source's presence cannot be determined
            and `force` is not set; nothing is pruned.
        RuntimeError: If `auto_snapshot` is set and an affected event cannot
            be snapshotted; nothing is pruned.
    """
    all_scope = isinstance(event_id, str) and event_id.lower() == "all"
    if not all_scope and session.get(AimbatEvent, event_id) is None:
        raise NoResultFound(f"No AimbatEvent found with id: {event_id}.")
    scope_label = "all events" if all_scope else f"event {event_id}"
    logger.info(f"Pruning {scope_label} (dry_run={dry_run}).")

    # A column query, not an ORM-object one: holding `AimbatDataSource`
    # instances in the session would make `session.delete(seismogram)` issue a
    # redundant ORM DELETE for a row the DB-level `ON DELETE CASCADE` already
    # removes.
    source_stmt = select(
        AimbatDataSource.sourcename,
        AimbatDataSource.datatype,
        AimbatDataSource.seismogram_id,
    ).join(AimbatSeismogram)
    if not all_scope:
        source_stmt = source_stmt.where(AimbatSeismogram.event_id == event_id)
    source_rows = session.exec(source_stmt).all()

    warnings: list[str] = []
    orphan_sources: dict[UUID, tuple[str, DataType]] = {}
    unprobeable: dict[DataType, int] = {}
    for sourcename, datatype_raw, seismogram_id in source_rows:
        sourcename = str(sourcename)
        datatype = DataType(datatype_raw)
        if not supports_source_probe(datatype):
            unprobeable[datatype] = unprobeable.get(datatype, 0) + 1
            continue
        try:
            present = source_present(sourcename, datatype, session)
        except SourceUnavailableError as exc:
            if not force:
                raise SourceUnavailableError(
                    f"Cannot determine whether data source {sourcename!r} "
                    + f"({datatype}) is present: {exc} Nothing was pruned. "
                    + "Restore the path, or re-run with force if these data "
                    + "are gone for good."
                ) from exc
            warnings.append(
                f"force: treating {sourcename!r} ({datatype}) as vanished "
                + f"even though its absence could not be confirmed ({exc})."
            )
            present = False
        if not present:
            orphan_sources[seismogram_id] = (sourcename, datatype)

    orphan_ids = set(orphan_sources)
    orphan_seismograms: list[AimbatSeismogram] = []
    if orphan_ids:
        orphan_seismograms = list(
            session.exec(
                select(AimbatSeismogram)
                .where(col(AimbatSeismogram.id).in_(orphan_ids))
                .options(
                    selectinload(rel(AimbatSeismogram.station)),
                    selectinload(rel(AimbatSeismogram.event)),
                )
            ).all()
        )

    affected_event_ids = {s.event_id for s in orphan_seismograms}
    affected_station_ids = {s.station_id for s in orphan_seismograms}

    events: list[AimbatEvent] = []
    if affected_event_ids:
        events = list(
            session.exec(
                select(AimbatEvent)
                .where(col(AimbatEvent.id).in_(affected_event_ids))
                .options(selectinload(rel(AimbatEvent.seismograms)))
            ).all()
        )
    stations: list[AimbatStation] = []
    if affected_station_ids:
        stations = list(
            session.exec(
                select(AimbatStation)
                .where(col(AimbatStation.id).in_(affected_station_ids))
                .options(selectinload(rel(AimbatStation.seismograms)))
            ).all()
        )

    emptied_events = [
        e for e in events if all(s.id in orphan_ids for s in e.seismograms)
    ]
    emptied_event_ids = {e.id for e in emptied_events}
    emptied_stations = [
        st for st in stations if all(s.id in orphan_ids for s in st.seismograms)
    ]

    pruned_event_ids = emptied_event_ids if prune_empty_events else set()
    events_needing_invalidation = [e for e in events if e.id not in pruned_event_ids]

    deleted_snapshots: dict[UUID, int] = {}
    if prune_empty_events:
        for e in emptied_events:
            count = session.exec(
                select(func.count(col(AimbatSnapshot.id))).where(
                    col(AimbatSnapshot.event_id) == e.id
                )
            ).one()
            if count:
                deleted_snapshots[e.id] = count

    cascaded_notes: dict[str, int] = {}
    seismogram_notes = _count_notes(session, "seismogram_id", orphan_ids)
    if seismogram_notes:
        cascaded_notes["seismogram"] = seismogram_notes
    if prune_empty_stations:
        station_notes = _count_notes(
            session, "station_id", {st.id for st in emptied_stations}
        )
        if station_notes:
            cascaded_notes["station"] = station_notes
    if prune_empty_events:
        event_notes = _count_notes(session, "event_id", emptied_event_ids)
        if event_notes:
            cascaded_notes["event"] = event_notes

    for datatype, count in unprobeable.items():
        warnings.append(
            f"Could not check {count} {datatype} source(s); reconcile incomplete."
        )

    report = PruneReport(
        orphan_seismograms=orphan_seismograms,
        orphan_sources=orphan_sources,
        unprobeable=unprobeable,
        emptied_stations=emptied_stations,
        emptied_events=emptied_events,
        events_needing_invalidation=events_needing_invalidation,
        deleted_snapshots=deleted_snapshots,
        cascaded_notes=cascaded_notes,
        warnings=warnings,
        dry_run=dry_run,
    )

    if dry_run or not orphan_seismograms:
        return report

    if confirm is not None and not confirm(report):
        logger.info("Prune cancelled before any deletion.")
        return report

    from ._iccs import _invalidate_event_quality, clear_iccs_cache
    from ._snapshot import create_snapshot

    invalidate_event_ids = [e.id for e in events_needing_invalidation]
    prune_station_ids = [s.id for s in emptied_stations] if prune_empty_stations else []
    prune_event_ids = [e.id for e in emptied_events] if prune_empty_events else []

    # Detach the eagerly loaded report graph now, while its relationships are
    # still populated. `create_snapshot` commits (expiring every attached
    # instance) and every deletion below reloads its target by ID, so nothing
    # past this point needs these instances bound to the session - but callers
    # still read `report.orphan_seismograms[*].station` and friends afterwards.
    session.expunge_all()

    if auto_snapshot:
        for event_uuid in invalidate_event_ids:
            snap_event = session.get(AimbatEvent, event_uuid)
            if snap_event is None:
                continue
            try:
                create_snapshot(
                    session,
                    snap_event,
                    comment="Automatic snapshot before prune",
                    automatic=True,
                )
            except Exception as exc:
                session.rollback()
                raise RuntimeError(
                    f"Aborting prune: could not snapshot event {event_uuid} "
                    + f"first ({exc}). Nothing was deleted."
                ) from exc

    for event_uuid in invalidate_event_ids:
        _invalidate_event_quality(session, event_uuid)

    # Every deletion below reloads its target by ID with no relationships
    # attached, so `session.delete()` leaves the 1:1 children (data source,
    # parameters, quality) to the DB-level `ON DELETE CASCADE` instead of also
    # issuing a redundant ORM DELETE for each - matching `core.delete_seismogram`.
    for seismogram_uuid in orphan_ids:
        seismogram = session.get(AimbatSeismogram, seismogram_uuid)
        if seismogram is not None:
            session.delete(seismogram)
    session.flush()
    for station_uuid in prune_station_ids:
        station = session.get(AimbatStation, station_uuid)
        if station is not None:
            session.delete(station)
    for event_uuid in prune_event_ids:
        event = session.get(AimbatEvent, event_uuid)
        if event is not None:
            session.delete(event)
    session.commit()

    clear_seismogram_data_cache()
    clear_iccs_cache()

    report.pruned = True
    pruned_stations = len(emptied_stations) if prune_empty_stations else 0
    pruned_events = len(emptied_events) if prune_empty_events else 0
    logger.info(
        f"Pruned {len(orphan_seismograms)} seismogram(s), "
        + f"{pruned_stations} station(s), {pruned_events} event(s)."
    )
    return report

reset_seismogram_parameters

reset_seismogram_parameters(
    session: Session, seismogram_id: UUID
) -> None

Reset an AimbatSeismogram's parameters to their default values.

All fields defined on AimbatSeismogramParametersBase are reset to the values produced by a fresh default instance, so newly added fields are picked up automatically. This includes t1, so a manual pick is discarded along with select and flip.

Parameters:

Name Type Description Default
session Session

Database session.

required
seismogram_id UUID

ID of seismogram to reset parameters for.

required

Raises:

Type Description
NoResultFound

If no seismogram with the given ID is found.

Source code in src/aimbat/core/_seismogram.py
def reset_seismogram_parameters(session: Session, seismogram_id: UUID) -> None:
    """Reset an AimbatSeismogram's parameters to their default values.

    All fields defined on AimbatSeismogramParametersBase are reset to the
    values produced by a fresh default instance, so newly added fields are
    picked up automatically. This includes `t1`, so a manual pick is
    discarded along with `select` and `flip`.

    Args:
        session: Database session.
        seismogram_id: ID of seismogram to reset parameters for.

    Raises:
        NoResultFound: If no seismogram with the given ID is found.
    """

    logger.info(f"Resetting parameters for seismogram {seismogram_id}.")

    seismogram = session.exec(
        select(AimbatSeismogram)
        .where(AimbatSeismogram.id == seismogram_id)
        .options(
            selectinload(rel(AimbatSeismogram.parameters)),
            selectinload(rel(AimbatSeismogram.event)).options(
                selectinload(rel(AimbatEvent.parameters)),
                selectinload(rel(AimbatEvent.seismograms)).selectinload(
                    rel(AimbatSeismogram.parameters)
                ),
            ),
        )
    ).one_or_none()
    if seismogram is None:
        raise NoResultFound(f"No AimbatSeismogram found with {seismogram_id=}")

    from ._snapshot import resync_quality

    defaults = AimbatSeismogramParametersBase()
    for field_name in AimbatSeismogramParametersBase.model_fields:
        setattr(seismogram.parameters, field_name, getattr(defaults, field_name))
    session.add(seismogram)
    resync_quality(session, seismogram.event)

resolve_event

resolve_event(
    session: Session, event_id: UUID | None = None
) -> AimbatEvent

Resolve an event from an explicit ID.

Parameters:

Name Type Description Default
session Session

SQL session.

required
event_id UUID | None

Optional event ID.

None

Returns:

Type Description
AimbatEvent

The specified event.

Raises:

Type Description
NoResultFound

If event_id is not given, or if no event with the given event_id exists.

Source code in src/aimbat/core/_event.py
def resolve_event(session: Session, event_id: UUID | None = None) -> AimbatEvent:
    """Resolve an event from an explicit ID.

    Args:
        session: SQL session.
        event_id: Optional event ID.

    Returns:
        The specified event.

    Raises:
        NoResultFound: If `event_id` is not given, or if no event with the
            given `event_id` exists.
    """
    if event_id is not None:
        logger.debug(f"Resolving event by explicit ID: {event_id}.")
        event = session.get(AimbatEvent, event_id)
        if event is None:
            raise NoResultFound(f"No AimbatEvent found with id: {event_id}.")
        return event

    raise NoResultFound("No event specified.")

resync_quality

resync_quality(
    session: Session,
    event: AimbatEvent,
    *,
    prefer_snapshot_id: UUID | None = None,
) -> SyncResult

Bring an event's live quality metrics back in line with its parameters.

The invalidation policy every parameter change follows: restore whatever a snapshot taken under the new parameters already measured, throw away the MCCC diagnostics that nothing can vouch for, and commit. Call it after changing event or seismogram parameters, with the change staged on session but not yet committed.

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

Event whose parameters have just changed.

required
prefer_snapshot_id UUID | None

Snapshot to prefer when several match, for a rollback restoring the snapshot it is rolling back to.

None

Returns:

Type Description
SyncResult

SyncResult recording which metric groups were repopulated.

Source code in src/aimbat/core/_snapshot.py
def resync_quality(
    session: Session,
    event: AimbatEvent,
    *,
    prefer_snapshot_id: UUID | None = None,
) -> SyncResult:
    """Bring an event's live quality metrics back in line with its parameters.

    The invalidation policy every parameter change follows: restore whatever a
    snapshot taken under the new parameters already measured, throw away the
    MCCC diagnostics that nothing can vouch for, and commit. Call it after
    changing event or seismogram parameters, with the change staged on
    `session` but not yet committed.

    Args:
        session: Database session.
        event: Event whose parameters have just changed.
        prefer_snapshot_id: Snapshot to prefer when several match, for a
            rollback restoring the snapshot it is rolling back to.

    Returns:
        `SyncResult` recording which metric groups were repopulated.
    """
    from ._iccs import clear_mccc_quality

    result = sync_from_matching_hash(
        session,
        event.id,
        iccs_hash=compute_iccs_hash(event),
        mccc_hash=compute_mccc_hash(event),
        prefer_snapshot_id=prefer_snapshot_id,
    )
    if not result.mccc_synced:
        clear_mccc_quality(session, event)
    session.commit()
    return result

rollback_to_snapshot

rollback_to_snapshot(
    session: Session, snapshot_id: UUID
) -> None

Rollback to an AIMBAT parameters snapshot.

Restores the event's and its seismograms' live parameters to the values frozen in the snapshot, then, in the same session, repopulates live quality metrics from a matching snapshot where possible (see sync_from_matching_hash), preferring the snapshot being rolled back to. MCCC quality that cannot be restored is cleared, mirroring the parameter setters.

Parameters:

Name Type Description Default
session Session

Database session.

required
snapshot_id UUID

Snapshot id.

required

Raises:

Type Description
ValueError

If no snapshot with the given ID is found.

Source code in src/aimbat/core/_snapshot.py
def rollback_to_snapshot(session: Session, snapshot_id: UUID) -> None:
    """Rollback to an AIMBAT parameters snapshot.

    Restores the event's and its seismograms' live parameters to the values
    frozen in the snapshot, then, in the same session, repopulates live
    quality metrics from a matching snapshot where possible (see
    `sync_from_matching_hash`), preferring the snapshot being rolled back to.
    MCCC quality that cannot be restored is cleared, mirroring the parameter
    setters.

    Args:
        session: Database session.
        snapshot_id: Snapshot id.

    Raises:
        ValueError: If no snapshot with the given ID is found.
    """
    logger.info(f"Rolling back to snapshot with id={snapshot_id}.")

    statement = (
        select(AimbatSnapshot)
        .where(AimbatSnapshot.id == snapshot_id)
        .options(
            selectinload(rel(AimbatSnapshot.event))
            .selectinload(rel(AimbatEvent.seismograms))
            .selectinload(rel(AimbatSeismogram.parameters)),
            selectinload(rel(AimbatSnapshot.event)).selectinload(
                rel(AimbatEvent.parameters)
            ),
            selectinload(rel(AimbatSnapshot.event_parameters_snapshot)),
            selectinload(
                rel(AimbatSnapshot.seismogram_parameters_snapshots)
            ).selectinload(rel(AimbatSeismogramParametersSnapshot.parameters)),
        )
    )
    snapshot = session.exec(statement).one_or_none()
    if snapshot is None:
        raise ValueError(f"No AimbatSnapshot found with {snapshot_id=}")

    # create object with just the parameters
    rollback_event_parameters = AimbatEventParametersBase.model_validate(
        snapshot.event_parameters_snapshot
    )
    logger.debug(
        f"Using event parameters snapshot with id={snapshot.event_parameters_snapshot.id} for rollback."
    )
    current_event_parameters = snapshot.event.parameters

    # setting attributes explicitly brings them into the session
    for k in AimbatEventParametersBase.model_fields.keys():
        v = getattr(rollback_event_parameters, k)
        logger.debug(f"Setting event parameter {k} to {v!r} for rollback.")
        setattr(current_event_parameters, k, v)

    session.add(current_event_parameters)

    for seismogram_parameters_snapshot in snapshot.seismogram_parameters_snapshots:
        current_seismogram_parameters = seismogram_parameters_snapshot.parameters
        if current_seismogram_parameters is None:
            # The seismogram was deleted after this snapshot was taken; its
            # frozen row is kept as history but there is nothing live to
            # roll back into.
            continue
        rollback_seismogram_parameters = AimbatSeismogramParametersBase.model_validate(
            seismogram_parameters_snapshot
        )
        logger.debug(
            f"Using seismogram parameters snapshot with id={seismogram_parameters_snapshot.id} for rollback."
        )
        for k in AimbatSeismogramParametersBase.model_fields.keys():
            v = getattr(rollback_seismogram_parameters, k)
            logger.debug(f"Setting seismogram parameter {k} to {v!r} for rollback.")
            setattr(current_seismogram_parameters, k, v)
        session.add(current_seismogram_parameters)

    resync_quality(session, snapshot.event, prefer_snapshot_id=snapshot_id)

run_iccs

run_iccs(
    session: Session,
    event: AimbatEvent,
    iccs: ICCS,
    autoflip: bool,
    autoselect: bool,
) -> IccsResult

Run the Iterative Cross-Correlation and Stack (ICCS) algorithm.

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent.

required
iccs ICCS

ICCS instance.

required
autoflip bool

If True, automatically flip seismograms to maximise cross-correlation.

required
autoselect bool

If True, automatically deselect seismograms whose cross-correlation falls below the threshold.

required

Returns:

Type Description
IccsResult

IccsResult from the algorithm run.

Source code in src/aimbat/core/_iccs.py
def run_iccs(
    session: Session, event: AimbatEvent, iccs: ICCS, autoflip: bool, autoselect: bool
) -> IccsResult:
    """Run the Iterative Cross-Correlation and Stack (ICCS) algorithm.

    Args:
        session: Database session.
        event: AimbatEvent.
        iccs: ICCS instance.
        autoflip: If True, automatically flip seismograms to maximise cross-correlation.
        autoselect: If True, automatically deselect seismograms whose cross-correlation
            falls below the threshold.

    Returns:
        IccsResult from the algorithm run.
    """

    logger.info(f"Running ICCS (autoflip={autoflip}, autoselect={autoselect}).")

    # `min_cc` is stored on the instance at construction and, unlike the MCCC
    # parameters, is not re-read per call. A bare `min_cc` change does not
    # rebuild the instance (it does not affect the stack), so refresh it here
    # before an autoselect run picks up a stale threshold.
    iccs.min_cc = event.parameters.min_cc
    result = iccs(autoflip=autoflip, autoselect=autoselect)
    n_iter = len(result.convergence)
    status = "converged" if result.converged else "did not converge"
    logger.info(f"ICCS {status} after {n_iter} iterations.")
    # Writing the picks back nulls `iccs_cc` via the invalidation triggers, so
    # `_write_iccs_stats` must come after that flush to repopulate it (see
    # `run_mccc` for the fuller ordering note). Both happen on `session`, so
    # the single commit below either publishes all of it or none of it.
    write_back_seismograms(session, iccs)
    _write_iccs_stats(session, event.id, iccs)
    session.commit()
    return result

run_mccc

run_mccc(
    session: Session,
    event: AimbatEvent,
    iccs: ICCS,
    all_seismograms: bool,
) -> McccResult

Run the Multi-Channel Cross-Correlation (MCCC) algorithm.

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent.

required
iccs ICCS

ICCS instance.

required
all_seismograms bool

If True, include deselected seismograms in the alignment.

required

Returns:

Type Description
McccResult

McccResult from the algorithm run.

Source code in src/aimbat/core/_iccs.py
def run_mccc(
    session: Session, event: AimbatEvent, iccs: ICCS, all_seismograms: bool
) -> McccResult:
    """Run the Multi-Channel Cross-Correlation (MCCC) algorithm.

    Args:
        session: Database session.
        event: AimbatEvent.
        iccs: ICCS instance.
        all_seismograms: If True, include deselected seismograms in the alignment.

    Returns:
        McccResult from the algorithm run.
    """

    logger.info(
        f"Running MCCC for event {event.id} (all_seismograms={all_seismograms})."
    )

    result = iccs.run_mccc(
        all_seismograms=all_seismograms,
        min_cc=event.parameters.mccc_min_cc,
        damping=event.parameters.mccc_damp,
    )
    # Order is load-bearing. Writing back `t1`/`flip`/`select` flushes an UPDATE
    # that fires the quality-invalidation triggers, nulling `iccs_cc` and every
    # MCCC quality column for this event. `_write_iccs_stats` must then
    # repopulate `iccs_cc`, and `_write_mccc_quality` the MCCC columns, in that
    # sequence. All three write on `session` and only the last line commits, so
    # a failure anywhere rolls the lot back rather than leaving the quality
    # tables half-nulled.
    write_back_seismograms(session, iccs)
    _write_iccs_stats(session, event.id, iccs)
    _write_mccc_quality(session, event.id, iccs, result, all_seismograms)
    session.commit()
    return result

save_note

save_note(
    session: Session,
    target: NoteTarget,
    target_id: UUID,
    content: str,
    *,
    expected_previous: str | None = None,
) -> bool

Save note content for the given entity, creating the note record if needed.

Parameters:

Name Type Description Default
session Session

Active database session.

required
target NoteTarget

Entity type, one of event, station, seismogram, snapshot.

required
target_id UUID

UUID of the target entity.

required
content str

Markdown note content to save.

required
expected_previous str | None

The content the caller last read before editing, if any. Compared against the note's content immediately before this save to detect a concurrent edit racing this one.

None

Returns:

Type Description
bool

True if expected_previous was given and didn't match the note's

bool

content right before this save - i.e. someone else's edit is about to

bool

be overwritten by this one. False otherwise.

Source code in src/aimbat/core/_note.py
def save_note(
    session: Session,
    target: NoteTarget,
    target_id: uuid.UUID,
    content: str,
    *,
    expected_previous: str | None = None,
) -> bool:
    """Save note content for the given entity, creating the note record if needed.

    Args:
        session: Active database session.
        target: Entity type, one of `event`, `station`, `seismogram`, `snapshot`.
        target_id: UUID of the target entity.
        content: Markdown note content to save.
        expected_previous: The content the caller last read before editing, if
            any. Compared against the note's content immediately before this
            save to detect a concurrent edit racing this one.

    Returns:
        True if `expected_previous` was given and didn't match the note's
        content right before this save - i.e. someone else's edit is about to
        be overwritten by this one. False otherwise.
    """
    attr = _note_id_attr(target)
    note = session.exec(select(AimbatNote).where(attr == target_id)).one_or_none()
    raced = (
        expected_previous is not None
        and (note.content if note is not None else "") != expected_previous
    )
    if note is None:
        note = AimbatNote(**{f"{target}_id": target_id, "content": content})
    else:
        note.content = content
    session.add(note)
    session.commit()
    return raced

set_event_parameter

set_event_parameter(
    session: Session,
    event_id: UUID,
    name: EventParameter,
    value: Timedelta | bool | float | int | str,
    *,
    validate_iccs: bool = False,
) -> None

Set event parameter value for the given event.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

UUID of the event to set the parameter value for.

required
name EventParameter

Name of the parameter.

required
value Timedelta | bool | float | int | str

Value to set.

required
validate_iccs bool

If True, attempt ICCS construction with the new value before committing. Raises and leaves the database unchanged on failure.

False

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

ValidationError

If value fails Pydantic validation for name.

IccsValidationError

If validate_iccs is True and ICCS construction is rejected by the new value.

Source code in src/aimbat/core/_event.py
def set_event_parameter(
    session: Session,
    event_id: UUID,
    name: EventParameter,
    value: Timedelta | bool | float | int | str,
    *,
    validate_iccs: bool = False,
) -> None:
    """Set event parameter value for the given event.

    Args:
        session: Database session.
        event_id: UUID of the event to set the parameter value for.
        name: Name of the parameter.
        value: Value to set.
        validate_iccs: If True, attempt ICCS construction with the new value
            before committing. Raises and leaves the database unchanged on failure.

    Raises:
        NoResultFound: If no event with the given ID is found.
        ValidationError: If `value` fails Pydantic validation for `name`.
        IccsValidationError: If `validate_iccs` is True and ICCS construction
            is rejected by the new value.
    """
    set_event_parameters(session, event_id, {name: value}, validate_iccs=validate_iccs)

set_event_parameters

set_event_parameters(
    session: Session,
    event_id: UUID,
    values: Mapping[
        EventParameter, Timedelta | bool | float | int | str
    ],
    *,
    validate_iccs: bool = False,
) -> None

Set several event parameters at once, validated as a group.

The whole update is validated against the merged parameter state, so a combination of values that no single-field order could reach (e.g. a new bandpass_fmin above the old bandpass_fmax alongside the new bandpass_fmax) is accepted.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

UUID of the event to update.

required
values Mapping[EventParameter, Timedelta | bool | float | int | str]

Parameter name -> new value. An empty mapping is a no-op.

required
validate_iccs bool

If True, attempt ICCS construction with the new values before committing. Raises and leaves the database unchanged on failure.

False

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

ValidationError

If any value fails Pydantic validation.

IccsValidationError

If validate_iccs is True and ICCS construction is rejected by the new values.

Source code in src/aimbat/core/_event.py
def set_event_parameters(
    session: Session,
    event_id: UUID,
    values: Mapping[EventParameter, Timedelta | bool | float | int | str],
    *,
    validate_iccs: bool = False,
) -> None:
    """Set several event parameters at once, validated as a group.

    The whole update is validated against the merged parameter state, so a
    combination of values that no single-field order could reach (e.g. a new
    `bandpass_fmin` above the old `bandpass_fmax` alongside the new
    `bandpass_fmax`) is accepted.

    Args:
        session: Database session.
        event_id: UUID of the event to update.
        values: Parameter name -> new value. An empty mapping is a no-op.
        validate_iccs: If True, attempt ICCS construction with the new values
            before committing. Raises and leaves the database unchanged on failure.

    Raises:
        NoResultFound: If no event with the given ID is found.
        ValidationError: If any value fails Pydantic validation.
        IccsValidationError: If `validate_iccs` is True and ICCS construction
            is rejected by the new values.
    """
    from ._iccs import validate_iccs_construction
    from ._snapshot import resync_quality

    updates = {str(name): value for name, value in values.items()}
    logger.debug(f"Setting {updates} for event {event_id=}.")

    event = session.exec(
        select(AimbatEvent)
        .where(AimbatEvent.id == event_id)
        .options(
            selectinload(rel(AimbatEvent.parameters)),
            selectinload(rel(AimbatEvent.seismograms)).selectinload(
                rel(AimbatSeismogram.parameters)
            ),
        )
    ).one_or_none()
    if event is None:
        raise NoResultFound(f"No AimbatEvent found with id: {event_id}.")

    if not updates:
        return

    parameters = AimbatEventParametersBase.model_validate(
        event.parameters,
        update=updates,
    )

    if validate_iccs:
        validate_iccs_construction(event, parameters=parameters)

    for name in updates:
        setattr(event.parameters, name, getattr(parameters, name))
    session.add(event)
    resync_quality(session, event)

set_seismogram_parameter

set_seismogram_parameter(
    session: Session,
    seismogram_id: UUID,
    name: SeismogramParameter,
    value: Timestamp | bool | str,
) -> None

Set parameter value for an AimbatSeismogram instance.

Parameters:

Name Type Description Default
session Session

Database session

required
seismogram_id UUID

Seismogram id.

required
name SeismogramParameter

Name of the parameter.

required
value Timestamp | bool | str

Value to set parameter to.

required

Raises:

Type Description
ValueError

If no seismogram with the given ID is found.

ValidationError

If value fails Pydantic validation for name.

Source code in src/aimbat/core/_seismogram.py
def set_seismogram_parameter(
    session: Session,
    seismogram_id: UUID,
    name: SeismogramParameter,
    value: Timestamp | bool | str,
) -> None:
    """Set parameter value for an AimbatSeismogram instance.

    Args:
        session: Database session
        seismogram_id: Seismogram id.
        name: Name of the parameter.
        value: Value to set parameter to.

    Raises:
        ValueError: If no seismogram with the given ID is found.
        ValidationError: If `value` fails Pydantic validation for `name`.
    """
    from ._snapshot import resync_quality

    logger.debug(
        f"Setting seismogram {name=} to {value=} in seismogram {seismogram_id=}."
    )

    seismogram = session.exec(
        select(AimbatSeismogram)
        .where(AimbatSeismogram.id == seismogram_id)
        .options(
            selectinload(rel(AimbatSeismogram.parameters)),
            selectinload(rel(AimbatSeismogram.event)).options(
                selectinload(rel(AimbatEvent.parameters)),
                selectinload(rel(AimbatEvent.seismograms)).selectinload(
                    rel(AimbatSeismogram.parameters)
                ),
            ),
        )
    ).one_or_none()
    if seismogram is None:
        raise ValueError(f"No AimbatSeismogram found with {seismogram_id=}")

    parameters = AimbatSeismogramParametersBase.model_validate(
        seismogram.parameters, update={name: value}
    )
    setattr(seismogram.parameters, name, getattr(parameters, name))
    session.add(seismogram)
    resync_quality(session, seismogram.event)

stamp_head

stamp_head(bind: Engine | Connection) -> None

Mark a database as being at the latest Alembic revision.

Writes Alembic's bookkeeping only, and runs no migration DDL. Intended for a database whose schema is already known to match head, e.g. immediately after create_project() creates a brand new one via SQLModel.metadata.create_all() rather than via a migration.

Parameters:

Name Type Description Default
bind Engine | Connection

Engine connected to the target database, or an open connection to it - the latter so the stamp can share a transaction with the schema it describes.

required
Source code in src/aimbat/core/_migrations.py
def stamp_head(bind: Engine | Connection) -> None:
    """Mark a database as being at the latest Alembic revision.

    Writes Alembic's bookkeeping only, and runs no migration DDL. Intended for
    a database whose schema is already known to match `head`, e.g.
    immediately after `create_project()` creates a brand new one via
    `SQLModel.metadata.create_all()` rather than via a migration.

    Args:
        bind: Engine connected to the target database, or an open connection
            to it - the latter so the stamp can share a transaction with the
            schema it describes.
    """
    from alembic import command

    command.stamp(_alembic_config(bind), "head")

sync_from_matching_hash

sync_from_matching_hash(
    session: Session,
    event_id: UUID,
    *,
    iccs_hash: str | None = None,
    mccc_hash: str | None = None,
    prefer_snapshot_id: UUID | None = None,
) -> SyncResult

Repopulate an event's live quality metrics from matching snapshots.

Considers only this event's snapshots. MCCC diagnostics are restored from the most recent snapshot whose mccc_hash equals mccc_hash and that froze MCCC quality; iccs_cc is restored from the most recent snapshot whose iccs_hash equals iccs_hash and that froze at least one iccs_cc value. The two are matched independently, so an ICCS-only snapshot can still repopulate iccs_cc. prefer_snapshot_id, when among the candidates, wins the tie-break.

Pending parameter changes are flushed first so the quality-invalidation triggers have fired before the restored values are written, and the restore queries re-read the live quality rows with populate_existing=True so they see what those triggers actually wrote rather than a stale identity-map value (see _RESTORE_POPULATE_EXISTING_NOTE). The caller owns the transaction and must commit.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

Event whose live quality should be synced.

required
iccs_hash str | None

Live ICCS hash to match; None skips iccs_cc restore.

None
mccc_hash str | None

Live MCCC hash to match; None skips MCCC restore.

None
prefer_snapshot_id UUID | None

Snapshot to prefer when several candidates match.

None

Returns:

Type Description
SyncResult

SyncResult recording which metric groups were repopulated. A matching

SyncResult

MCCC snapshot whose event-level restore had to be skipped counts as

SyncResult

unsynced, so the caller still clears the stale diagnostics.

Source code in src/aimbat/core/_snapshot.py
def sync_from_matching_hash(
    session: Session,
    event_id: UUID,
    *,
    iccs_hash: str | None = None,
    mccc_hash: str | None = None,
    prefer_snapshot_id: UUID | None = None,
) -> SyncResult:
    """Repopulate an event's live quality metrics from matching snapshots.

    Considers only this event's snapshots. MCCC diagnostics are restored from
    the most recent snapshot whose `mccc_hash` equals `mccc_hash` and that
    froze MCCC quality; `iccs_cc` is restored from the most recent snapshot
    whose `iccs_hash` equals `iccs_hash` and that froze at least one `iccs_cc`
    value. The two are matched independently, so an ICCS-only snapshot can
    still repopulate `iccs_cc`. `prefer_snapshot_id`, when among the
    candidates, wins the tie-break.

    Pending parameter changes are flushed first so the quality-invalidation
    triggers have fired before the restored values are written, and the restore
    queries re-read the live quality rows with `populate_existing=True` so they
    see what those triggers actually wrote rather than a stale identity-map
    value (see `_RESTORE_POPULATE_EXISTING_NOTE`). The caller owns the
    transaction and must commit.

    Args:
        session: Database session.
        event_id: Event whose live quality should be synced.
        iccs_hash: Live ICCS hash to match; `None` skips `iccs_cc` restore.
        mccc_hash: Live MCCC hash to match; `None` skips MCCC restore.
        prefer_snapshot_id: Snapshot to prefer when several candidates match.

    Returns:
        `SyncResult` recording which metric groups were repopulated. A matching
        MCCC snapshot whose event-level restore had to be skipped counts as
        unsynced, so the caller still clears the stale diagnostics.
    """
    session.flush()

    snapshots = get_snapshots(session, event_id)

    mccc_synced = False
    if mccc_hash is not None:
        mccc_candidate = _pick_candidate(
            [
                s
                for s in snapshots
                if s.mccc_hash == mccc_hash
                and s.event_quality_snapshot is not None
                and s.event_quality_snapshot.mccc_rmse is not None
            ],
            prefer_snapshot_id,
        )
        if mccc_candidate is not None:
            mccc_synced = _restore_mccc_quality(session, mccc_candidate)
        else:
            logger.debug("No snapshot with matching MCCC hash and quality found.")

    iccs_synced = False
    if iccs_hash is not None:
        iccs_candidate = _pick_candidate(
            [
                s
                for s in snapshots
                if s.iccs_hash == iccs_hash
                and any(q.iccs_cc is not None for q in s.seismogram_quality_snapshots)
            ],
            prefer_snapshot_id,
        )
        if iccs_candidate is not None:
            _restore_iccs_cc(session, iccs_candidate)
            iccs_synced = True
        else:
            logger.debug("No snapshot with matching ICCS hash and iccs_cc found.")

    return SyncResult(mccc_synced=mccc_synced, iccs_synced=iccs_synced)

sync_iccs_parameters

sync_iccs_parameters(
    session: Session, event: AimbatEvent, iccs: ICCS
) -> None

Sync an existing ICCS instance's parameters from the database.

Updates event-level and per-seismogram parameters without re-reading waveform data. Use this after operations that change parameters but not the seismogram list (e.g. rolling back to a snapshot).

Parameters:

Name Type Description Default
session Session

Database session.

required
event AimbatEvent

AimbatEvent.

required
iccs ICCS

ICCS instance to update in-place.

required
Source code in src/aimbat/core/_iccs.py
def sync_iccs_parameters(session: Session, event: AimbatEvent, iccs: ICCS) -> None:
    """Sync an existing ICCS instance's parameters from the database.

    Updates event-level and per-seismogram parameters without re-reading waveform
    data. Use this after operations that change parameters but not the
    seismogram list (e.g. rolling back to a snapshot).

    Args:
        session: Database session.
        event: AimbatEvent.
        iccs: ICCS instance to update in-place.
    """

    logger.debug(f"Syncing ICCS parameters from database for event {event.id}.")

    event_params = AimbatEventParametersBase.model_validate(event.parameters)
    for field_name in AimbatEventParametersBase.model_fields:
        if field_name in _NON_ICCS_EVENT_PARAMETER_FIELDS:
            continue
        if not hasattr(iccs, field_name):
            raise AttributeError(
                f"ICCS has no attribute {field_name!r}; if this field is not "
                + "meant to be synced to ICCS, add it to "
                + "_NON_ICCS_EVENT_PARAMETER_FIELDS"
            )
        setattr(iccs, field_name, getattr(event_params, field_name))

    for iccs_seis in iccs.seismograms:
        db_seis = session.get(AimbatSeismogram, iccs_seis.extra["id"])
        if db_seis is not None:
            seis_params = AimbatSeismogramParametersBase.model_validate(
                db_seis.parameters
            )
            for field_name in AimbatSeismogramParametersBase.model_fields:
                setattr(iccs_seis, field_name, getattr(seis_params, field_name))

    iccs.clear_cache()

toggle_event_completed

toggle_event_completed(
    session: Session, event_id: UUID
) -> bool

Flip the completed flag on an event's parameters.

completed is excluded from the parameters hash used for snapshot matching (it does not affect seismogram processing), so this does not go through set_event_parameter's snapshot-sync/MCCC-invalidation path.

Parameters:

Name Type Description Default
session Session

Database session.

required
event_id UUID

UUID of the event to toggle.

required

Returns:

Type Description
bool

The new value of the completed flag.

Raises:

Type Description
NoResultFound

If no event with the given ID is found.

Source code in src/aimbat/core/_event.py
def toggle_event_completed(session: Session, event_id: UUID) -> bool:
    """Flip the `completed` flag on an event's parameters.

    `completed` is excluded from the parameters hash used for snapshot
    matching (it does not affect seismogram processing), so this does not
    go through `set_event_parameter`'s snapshot-sync/MCCC-invalidation path.

    Args:
        session: Database session.
        event_id: UUID of the event to toggle.

    Returns:
        The new value of the `completed` flag.

    Raises:
        NoResultFound: If no event with the given ID is found.
    """
    logger.debug(f"Toggling completed flag for event {event_id}.")

    event = session.get(AimbatEvent, event_id)
    if event is None:
        raise NoResultFound(f"No AimbatEvent found with id: {event_id}.")

    event.parameters.completed = not event.parameters.completed
    session.add(event)
    session.commit()
    return event.parameters.completed

upgrade_project

upgrade_project(engine: Engine) -> None

Upgrade the project database to the latest Alembic revision.

Handles the normal case (an already-stamped database with pending migrations), the legacy case (a pre-Alembic database with no alembic_version table), and an unrecognised-revision case automatically:

  • If the database is already stamped at a revision this installation's migration scripts recognise as an ancestor of head (including head itself), this simply runs any pending migrations.
  • If it's stamped at a revision this installation doesn't recognise at all (e.g. created by a different/newer AIMBAT release, or a corrupted alembic_version value), this raises SchemaMismatchError immediately rather than calling into Alembic, which would otherwise fail with a raw, unhelpful "Can't locate revision identified by ..." error.
  • If it has never been stamped but already has some schema (a pre-Alembic database), that schema is checked against every known revision, not just head (see _find_matching_revision) - a database that skipped an AIMBAT release and matches an older revision is stamped at that revision and then upgraded the rest of the way, not rejected just because it isn't already at head. If it doesn't match any known revision at all, this raises SchemaMismatchError rather than guessing. Silently stamping a database whose schema doesn't actually match would tell Alembic's bookkeeping "this is up to date" while it is actually missing columns/tables, turning a loud, safe failure into a silent one.
  • If it has never been stamped and has no schema at all (a genuinely empty/new database), it is built from scratch by running the full migration chain, with no comparison or stamping shortcut needed, since there is nothing pre-existing that could conflict.

Parameters:

Name Type Description Default
engine Engine

The SQLAlchemy/SQLModel Engine instance connected to the target database.

required

Raises:

Type Description
SchemaMismatchError

If the database has no Alembic version history and its live schema doesn't match any known AIMBAT schema version, or if it's stamped at a revision this installation's migration scripts don't recognise at all.

Source code in src/aimbat/core/_migrations.py
def upgrade_project(engine: Engine) -> None:
    """Upgrade the project database to the latest Alembic revision.

    Handles the normal case (an already-stamped database with pending
    migrations), the legacy case (a pre-Alembic database with no
    `alembic_version` table), and an unrecognised-revision case
    automatically:

    - If the database is already stamped at a revision this installation's
      migration scripts recognise as an ancestor of `head` (including
      `head` itself), this simply runs any pending migrations.
    - If it's stamped at a revision this installation *doesn't* recognise
      at all (e.g. created by a different/newer AIMBAT release, or a
      corrupted `alembic_version` value), this raises `SchemaMismatchError`
      immediately rather than calling into Alembic, which would otherwise
      fail with a raw, unhelpful "Can't locate revision identified by ..."
      error.
    - If it has never been stamped but already has *some* schema (a
      pre-Alembic database), that schema is checked against every known
      revision, not just `head` (see `_find_matching_revision`) - a
      database that skipped an AIMBAT release and matches an older
      revision is stamped at *that* revision and then upgraded the rest of
      the way, not rejected just because it isn't already at head. If it
      doesn't match any known revision at all, this raises
      `SchemaMismatchError` rather than guessing. Silently stamping a
      database whose schema doesn't actually match would tell Alembic's
      bookkeeping "this is up to date" while it is actually missing
      columns/tables, turning a loud, safe failure into a silent one.
    - If it has never been stamped *and* has no schema at all (a genuinely
      empty/new database), it is built from scratch by running the full
      migration chain, with no comparison or stamping shortcut needed, since
      there is nothing pre-existing that could conflict.

    Args:
        engine: The SQLAlchemy/SQLModel Engine instance connected to the
            target database.

    Raises:
        SchemaMismatchError: If the database has no Alembic version history
            and its live schema doesn't match any known AIMBAT schema
            version, or if it's stamped at a revision this installation's
            migration scripts don't recognise at all.
    """
    from alembic import command
    from sqlalchemy import inspect

    config = _alembic_config(engine)
    current_revision = get_current_revision(engine)

    if (
        current_revision is not None
        and current_revision not in _known_ancestors_of_head()
    ):
        raise SchemaMismatchError(
            f"The database at {engine.url} is stamped at schema version '"
            + f"{current_revision}', which this AIMBAT installation doesn't "
            + "recognise - it may have been created by a different or newer AIMBAT "
            + "release, or the version record may be corrupted. This can't be "
            + "resolved automatically. Manual intervention is required."
        )

    if current_revision is None:
        with engine.connect() as connection:
            has_existing_schema = bool(inspect(connection).get_table_names())

        if has_existing_schema:
            matched_revision = _find_matching_revision(engine)

            if matched_revision is None:
                raise SchemaMismatchError(
                    f"The database at {engine.url} has no Alembic version history, and its"
                    + " schema doesn't match any known AIMBAT schema version, so it can't "
                    + "be upgraded automatically. This usually means the project was "
                    + "created by a very old version of AIMBAT. Manual intervention is "
                    + "required."
                )

            logger.info(
                f"No Alembic history found for {engine.url}, but its schema matches "
                + f"revision {matched_revision!r} exactly - stamping at that revision "
                + "before upgrading the rest of the way."
            )
            command.stamp(config, matched_revision)
        # else: genuinely empty database - fall through to `command.upgrade`
        # below, which builds the schema from scratch via the full
        # migration chain, exactly like a brand new install.

    _backup_before_upgrade(engine, current_revision)
    command.upgrade(config, "head")

validate_iccs_construction

validate_iccs_construction(
    event: AimbatEvent,
    parameters: AimbatEventParametersBase | None = None,
) -> None

Try to construct an ICCS instance for the event without caching the result.

Use this to check whether the event's current (possibly uncommitted) parameters are compatible with ICCS construction before persisting them to the database.

Parameters:

Name Type Description Default
event AimbatEvent

AimbatEvent.

required
parameters AimbatEventParametersBase | None

Optional AimbatEventParametersBase to use instead of the live event parameters (useful for validation).

None

Raises:

Type Description
IccsValidationError

If the parameters are rejected by ICCS construction.

Source code in src/aimbat/core/_iccs.py
def validate_iccs_construction(
    event: AimbatEvent, parameters: AimbatEventParametersBase | None = None
) -> None:
    """Try to construct an ICCS instance for the event without caching the result.

    Use this to check whether the event's current (possibly uncommitted) parameters
    are compatible with ICCS construction before persisting them to the database.

    Args:
        event: AimbatEvent.
        parameters: Optional AimbatEventParametersBase to use instead of the live
            event parameters (useful for validation).

    Raises:
        IccsValidationError: If the parameters are rejected by ICCS construction.
    """
    try:
        _build_iccs(event, parameters=parameters)
    except ValueError as exc:
        # pysmo's attrs validators and pydantic both reject bad parameter
        # values with a ValueError. Anything else - a missing data source, an
        # AttributeError - is not the parameters' fault and propagates as
        # itself rather than being relabelled.
        logger.exception("ICCS validation failed during parameter check.")
        raise IccsValidationError(f"ICCS validation failed: {exc}") from exc

write_back_seismograms

write_back_seismograms(
    session: Session, iccs: ICCS
) -> None

Write t1, flip, and select from ICCS seismograms back to the database.

Flushes but does not commit; the caller owns the transaction and must commit it. The flush fires the quality-invalidation triggers (SQLite AFTER UPDATE triggers run at statement execution), but callers that repopulate quality afterwards (some from their own separate sessions) depend on controlling when that nulling becomes visible.

Parameters:

Name Type Description Default
session Session

Database session.

required
iccs ICCS

ICCS instance whose seismograms carry UUIDs in their extra dict.

required
Source code in src/aimbat/core/_iccs.py
def write_back_seismograms(session: Session, iccs: ICCS) -> None:
    """Write t1, flip, and select from ICCS seismograms back to the database.

    Flushes but does not commit; the caller owns the transaction and must
    commit it. The flush fires the quality-invalidation triggers (SQLite
    AFTER UPDATE triggers run at statement execution), but callers that
    repopulate quality afterwards (some from their own separate sessions)
    depend on controlling when that nulling becomes visible.

    Args:
        session: Database session.
        iccs: ICCS instance whose seismograms carry UUIDs in their extra dict.
    """
    logger.debug(f"Writing back {len(iccs.seismograms)} seismogram parameters to DB.")

    for seis in iccs.seismograms:
        db_seis = session.get(AimbatSeismogram, seis.extra["id"])
        if db_seis is not None:
            db_seis.parameters.t1 = seis.t1
            db_seis.parameters.flip = seis.flip
            db_seis.parameters.select = seis.select
    session.flush()