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:
- 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.
- Models. SQLModel classes mirroring the
database schema (
aimbat.models). The main ones areAimbatEvent,AimbatStation, andAimbatSeismogram, plus their parameter and quality tables. Columns and relationships are plain attributes; sets of records come fromselectqueries. - 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.