Skip to content

API basics

The building blocks behind the AIMBAT interfaces (CLI, shell, TUI) are also available for custom scripts and workflows.

API components

The API has three primary parts:

  1. Session. The working connection to the project database that every read or write goes through. It also defines the transaction boundary: changes become permanent only when the session commits.
  2. Models. SQLModel classes mirroring the database schema (aimbat.models). The main ones are AimbatEvent, AimbatStation, and AimbatSeismogram, plus their parameter and quality tables. Columns and relationships are plain attributes; sets of records come from select queries.
  3. Core functions. High-level operations on those models (aimbat.core): event selection, parameter changes, ICCS and MCCC runs, snapshots. The CLI, shell, and TUI call the same functions, and the mutating ones commit internally.

Starting a session

Every database operation needs a Session. It is opened as a context manager so it is always closed cleanly:

from sqlmodel import Session
from aimbat.db import engine

with Session(engine) as session:
    # query or modify data here
    pass

Changes reach disk only on session.commit(), or when a core function commits internally. An exception before that rolls the session back and discards the changes.

aimbat.db.engine reads AIMBAT_PROJECT (or the default aimbat.db) exactly as the other interfaces do, so a script that imports it points at the same database as the CLI. The Project page covers the configuration options.

Selecting events and stations

Given a record's ID, Session.get fetches it directly:

from uuid import UUID
from sqlmodel import Session
from aimbat.db import engine
from aimbat.models import AimbatEvent

with Session(engine) as session:
    event = session.get(AimbatEvent, UUID("..."))

Queries by any other criterion use SQLModel's select with a where clause:

from sqlmodel import Session, select
from aimbat.db import engine
from aimbat.models import AimbatEvent, AimbatSeismogram

with Session(engine) as session:
    events = session.exec(select(AimbatEvent)).all()
    for event in events:
        print(f"{event.time}  {len(event.seismograms)} seismograms")

    # Filter — seismograms marked as selected
    selected = session.exec(
        select(AimbatSeismogram).where(
            AimbatSeismogram.parameters.has(select=True)  # type: ignore[attr-defined]
        )
    ).all()

aimbat.core also has cross-selections for the common cases: get_stations_in_event and get_events_using_station.

Reading attributes

Column fields (event.time, station.network, seismogram.t0, ...) are plain Python attributes, read directly off any record a query returns.

A few frequently needed counts are precomputed as column properties, so reading them costs nothing extra:

event.seismogram_count      # seismograms belonging to this event
event.station_count         # distinct stations recording this event
station.seismogram_count    # seismograms recorded at this station

Relationships

Relationships (event.seismograms, seismogram.station, seismogram.parameters, ...) are attributes too, but the first access to one issues a query for the related records. That's cheap for a single record. Over a large result set it becomes one query per row:

with Session(engine) as session:
    seismograms = session.exec(select(AimbatSeismogram)).all()

    for seis in seismograms:
        print(seis.station.name)  # one SELECT per seismogram

Eager loading with SQLAlchemy's selectinload fetches the related records for the whole result set in one extra query. The rel helper in aimbat.utils casts a SQLModel relationship attribute to the type selectinload expects:

from sqlalchemy.orm import selectinload
from sqlmodel import Session, select

from aimbat.db import engine
from aimbat.models import AimbatSeismogram
from aimbat.utils import rel

with Session(engine) as session:
    seismograms = session.exec(
        select(AimbatSeismogram).options(
            selectinload(rel(AimbatSeismogram.station)),
            selectinload(rel(AimbatSeismogram.event)),
        )
    ).all()

    for seis in seismograms:
        print(seis.station.name, seis.event.time)  # no further queries

Committing changes

Core functions commit internally. Querying and mutating models directly needs both add() and commit(): a query result is already tracked by the session, but a newly constructed object is not:

with Session(engine) as session:
    event = session.exec(select(AimbatEvent)).first()
    event.latitude = 37.5   # already tracked by the session: no add() needed
    session.commit()        # required, or the change rolls back on exit

    new_event = AimbatEvent(time=..., latitude=..., longitude=...)
    session.add(new_event)  # required: not yet tracked by the session
    session.commit()

Each commit is a disk write. Calling a core function that commits internally, such as set_seismogram_parameter, once per seismogram in a loop costs one commit per iteration. For changes across many records, mutating them directly and committing once after the loop avoids that.

Working with seismogram data

The waveform samples for a seismogram are not a database column. They live in the data source (the SAC or miniSEED file), and AimbatSeismogram.data reads and writes them there.

with Session(engine) as session:
    seis = session.exec(select(AimbatSeismogram)).first()

    samples = seis.data          # read-only NumPy array
    seis.data = samples * 2.0    # staged, not yet on disk
    session.commit()             # now written to the data source

Reading data returns a read-only array; seis.data[0] = 1.0 raises. Assigning data stages the write in the session, like any other model change: it reaches the data source when the session commits and is discarded if the session rolls back. Reads through the same session see the staged value before the commit; other sessions see the old one until then.

AIMBAT itself never writes waveform samples — every processing step writes parameters to the database. This matters only for custom scripts that replace a seismogram's waveform.

Embedding AIMBAT in a larger application

Importing aimbat.models (or aimbat.db, or any AIMBAT code that reads the project database) registers commit and rollback listeners on SQLAlchemy's Session class for the whole process, so that a staged data write is never silently lost. Every Session in the process, including those of unrelated libraries, then runs a short check on each commit and rollback; the work only happens for a session that actually staged a waveform write.