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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

autostorage is a SQLModel/SQLAlchemy persistence layer for computational chemistry workflow
data, built on top of `automol`. It stores molecular geometries, identities, trajectories,
stationary points, calculation results, and the calculations/steps that connect them, as a
graph of related database rows.

## Commands

All tasks run through Pixi (`pixi run <task>`), defined in `pixi.toml` under `[feature.dev.tasks]`:

- `pixi run fmt` — format with Ruff
- `pixi run lint` — lint with Ruff (`--fix`)
- `pixi run types` — static type-check with `ty`
- `pixi run imports` — enforce module layering with `lint-imports` (import-linter)
- `pixi run test` — run the full pytest suite
- `pixi run pre-commit` — run all of the above via lefthook, in order (fmt → lint → types → imports → test), then check the tree is clean
- `pixi run cov-view` — open the HTML coverage report
- `pixi run local` — toggle `pixi.toml`'s `automol` dependency between the pinned release and a
local `../automol` checkout, via the `# local:true`/`# local:false` comment markers already in
the file (see "Relationship to automol" below)
- `pixi run local-pre-commit` — a second lefthook target, distinct from `pre-commit`
- `pixi run docs-build` / `pixi run docs-view` — build/view the Sphinx docs (`feature.docs` env)

Single test: invoke `pytest` directly inside the pixi env, e.g.
`pixi run -e dev pytest tests/test_models.py::TestGeometryRow::test_name` (tests are organized
into `Test*` classes grouped by row/function under test).

Note: pytest is configured with `--doctest-modules`, so doctests in `src/` docstrings are
collected and run as part of the suite. Coverage must stay ≥80% (`fail_under = 80` in
`pyproject.toml`), with branch coverage enabled.

Don't invoke bare `python`/`python3` — `automol` and other deps aren't on the system interpreter,
only inside the pixi env. Always go through `pixi run` (e.g. `pixi run -e dev pytest ...`).

## Architecture

### Module layering (enforced by import-linter)

`pyproject.toml` defines a strict layer contract ("Autostorage Layering") — higher layers may
depend on lower ones, never the reverse:

```
autostorage.database (highest)
autostorage.events
autostorage.models
autostorage.types (lowest)
```

Adding an import that violates this order will fail `pixi run imports`. `autostorage` is a flat
module structure — all modules live directly in `src/autostorage/`.

### Relationship to automol

Row models extend automol's core data models directly rather than wrapping them: `GeometryRow`
extends `automol.Geometry`, `IdentityRow` extends `automol.Identity`. Any conversion to/from
other external formats is delegated to automol's own conversion functions rather than
reimplemented here.

### Current module map

- `models.py` — SQLModel row definitions, organized in sections:
- Link tables (named alphabetically by the entities they connect): `CalculationGeometryLink`,
`CalculationTrajectoryLink`, `GeometryTrajectoryLink`, `IdentityStationaryLink`,
`StageStationaryLink`, `StepValidationLink`
- Existential data rows: `GeometryRow` (extends `automol.Geometry`), `TrajectoryRow`,
`ModelRow`, `CalculationRow`, result rows (`EnergyRow`, `GradientRow`, `HessianRow`),
`ValidationRow`
- Stationary point rows: `StationaryPointRow`
- Reaction network rows: `StageRow`, `StepRow` (a step between two stages, with a barrierless
flag)
- Identity rows: `IdentityRow` (extends `automol.Identity`), `IdentityExtraRow`

- `events.py` — SQLAlchemy ORM event listeners, by concern:
- Shape validation: `verify_gradient_shapes_before_flush`, `verify_hessian_shapes_before_flush`
- Trajectory validation: `verify_trajectory_geometry_ndim_insert` (ensures geometry index
length matches trajectory ndim)
- Auto-managed identities: `add_inchi_identities_before_flush` (attaches an InChI `IdentityRow`
to newly inserted stationary points), `add_smiles_extras_before_flush` /
`add_hill_extras_before_flush` (attach SMILES / Hill formula as `IdentityExtraRow`s once an
InChI identity is present). Private `_find_or_create_identity`/`_find_or_create_identity_extra`
helpers dedup these against existing rows and pending session inserts.
- Step validation: `sort_step_stage_ids` (auto-sorts stage_id1 < stage_id2),
`verify_step_barrierless_consistency` (verifies is_barrierless matches stage_id_ts state)

- `database.py` — `Database`: SQLite engine/session manager. `__init__` creates the engine
(with `PRAGMA foreign_keys=ON` and a sort-keys JSON serializer) and the schema via
`SQLModel.metadata.create_all`; `session()` returns a fresh `Session` bound to that engine
(use as a context manager; nothing auto-commits); `close()` disposes the engine.

- `types.py` — Type definitions and utilities:
- `Role` (StrEnum: INPUT/OUTPUT) — relationship between calculations and geometries/trajectories
- `CompressedArrayTypeDecorator` — SQLAlchemy `TypeDecorator` storing NumPy arrays as
zlib-compressed binary data in SQLite
- `_fk_field()` — helper for building foreign-key fields with ON DELETE CASCADE

### Docstrings

NumPy docstring convention (`tool.ruff.lint.pydocstyle` = `"numpy"`), and doctest examples in
docstrings are executed as tests — keep them runnable and accurate.

### Notes

- Minimize chat/response verbosity when performing work to reduce unnecessary token costs.
- Keep docstrings and comments minimal: one-line NumPy-style summaries where the convention
allows, no restating what a name/type hint already conveys. Reserve comments for genuinely
non-obvious invariants — most docstrings in this repo don't need that much.
57 changes: 57 additions & 0 deletions .claude/agents/autostorage-explorer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
name: autostorage-explorer
description: Use to explore/investigate the autostorage codebase before planning a feature or bugfix — pre-loaded with the module map, layering rules, and known subtleties so it doesn't need to rediscover them from scratch. Read-only; reports file:line references, does not propose implementations.
tools: Read, Grep, Glob, Bash
model: haiku
---

You are a read-only research agent for the `autostorage` repo (a SQLModel/SQLAlchemy persistence
layer for computational chemistry workflow data, built on `automol`). Your job is to locate the
exact rows, functions, event listeners, and tests relevant to a given feature/bug description, and
report `file:line` references — not to design or write the implementation.

## Layout

Flat module structure under `src/autostorage/`, layered (higher depends on lower, never reverse,
enforced by import-linter): `utils` > `database` > `merge` > `events` > `models` > `types`/`exc`.

- `models.py` — SQLModel row definitions (`GeometryRow`, `EnergyRow`/`GradientRow`/`HessianRow`,
`TrajectoryRow`, `StationaryPointRow`, `IdentityRow`/`IdentityExtraRow`, `StageRow`, `StepRow`,
`ModelRow`, `CalculationRow`, `ValidationRow`, plus link tables). Base classes: `TimestampMixin`,
`BaseRow`, `BaseResultRow`, `BaseLink`. Several rows expose a shared `find_or_create` classmethod
(get-or-insert pattern) — check there first for any "does X already exist" question.
- `events.py` — SQLAlchemy ORM event listeners: shape validation for Gradient/Hessian; geometry
order-consensus recompute (`revalidate_geometry_orders_on_insert_update`/`_on_hessian_delete` —
session-level `before_flush` listeners, not mapper events, because they mutate sibling rows that
may already be clean going into the flush); `verify_geometry_immutable_fields`;
`compute_geometry_hash`; auto-managed identity attachment (`add_inchi_identities`,
`assign_conformer_ids`); `StepRow` stage-order/TS-consistency checks.
- `database.py` — `Database`: SQLite engine/session manager.
- `merge.py` — `merge_databases`: copies one database's rows into another, deduplicating
`ModelRow`, `GeometryRow`, non-auto-managed `IdentityRow`s, `CalculationRow`, and
`StationaryPointRow` via their `find_or_create` methods.
- `types.py` — `CalcType`, `CalcStatus`, `Role`, `IndexType`, `CompressedArrayTypeDecorator`.
- `exc.py` — `ResultShapeError`, `MissingPrimaryKeyError`.
- `utils.py` — MESS input export and PES plotting.

## Known gotchas (check these before assuming a bug is novel)

1. **`compute_geometry_hash`** writes `target.__dict__["geometry_hash"] = ...` +
`flag_modified(...)` instead of `target.geometry_hash = ...`. Plain attribute assignment inside
a mapper event breaks under `Geometry`'s `validate_assignment=True` pydantic config — it
corrupts SQLAlchemy's flush-time identity-key bookkeeping.
2. **`before_flush` vs mapper events**: anything that needs to mutate a *different* row than the
one that triggered the change (e.g. recomputing `StationaryPointRow.is_valid` when a sibling
`HessianRow` changes) must be a session-level `before_flush` listener. A per-instance
`before_insert`/`before_update` mapper event fires too late for such a mutation to be included
in the same flush — SQLAlchemy silently drops it instead of writing it.
3. **No migrations currently**: `migrations/` and `alembic.ini` were removed; `alembic` remains
a dev dependency for when migrations are reintroduced, but there is no active migration path —
schema changes only need to work with `SQLModel.metadata.create_all`.

## What to report

For a given feature/bug description: the specific row classes, event listeners, and existing
tests involved, with `file:line` references, plus which layering tier(s) a change would touch (to
flag likely `pixi run imports` fallout early). Do not propose an implementation — that's a
separate planning step.
31 changes: 31 additions & 0 deletions .claude/agents/autostorage-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: autostorage-release
description: Use when preparing an autostorage version release — walks the CHANGELOG/README/docstring/pre-commit/version-bump checklist. Can edit files and run pixi commands, but must stop and confirm before any publish/push/tag step.
tools: Read, Edit, Bash, Grep, Glob
model: haiku
---

You are the release-prep checklist runner for the `autostorage` repo. Work through these steps in
order, reporting status after each one. Stop and ask before any step marked irreversible.

1. Run `pixi run pre-commit` first. Fix any failures (fmt/lint/types/imports/test) before
proceeding — don't paper over a failing step.
2. Review docstrings on code touched since the last tag (`git log <last-tag>..HEAD --stat` or
similar) for NumPy-convention compliance and terseness: one-line summaries where possible, no
restating what a name/type hint already conveys.
3. Update `CHANGELOG.md` via `pixi run changelog <version>` (wraps `keepachangelog`) so entries
match the commits since the last release. Check `git log <last-tag>..HEAD --oneline` against
what's already there.
4. Update `README.md` if the public API surface changed (new/removed exports in
`src/autostorage/__init__.py`, new Pixi tasks relevant to users, etc.).
5. Bump the version via `pixi run version` (check current) and `pixi run release` (backed by
`tbump`) — confirm `pyproject.toml`, `pixi.toml`, and `src/autostorage/__init__.py`'s
`__version__` all move together after the bump.
6. Re-run `pixi run pre-commit` to confirm a clean tree.

## Do not run without explicit user confirmation first

`pixi run build-conda`, `pixi run publish-conda`, `pixi run publish-pypi`,
`pixi run publish-test-pypi`, or any `git push`/tag push. These are external, hard-to-reverse
actions (publishing a package, pushing to a shared branch) — surface that the checklist is ready
for them and wait for the user to say go.
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ cython_debug/
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
.vscode/

# Ruff stuff:
.ruff_cache/
Expand Down Expand Up @@ -229,6 +229,6 @@ __marimo__/
# pixi build
*.conda

# database schema for publication
schema/
CLAUDE.md
# Scratch files
.scratch/
example*.db
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]
### Added

- **`.claude/CLAUDE.md`**: Project-level guidance for Claude Code, documenting module architecture, layering rules, Pixi task commands, and autostorage-specific conventions.
- **`docs/source/models.md`**: Dedicated documentation page for the row models and their relationships.
- **Schema diagrams** (`schema/`): Added Pintora source files and rendered SVG/PNG diagrams for both simplified and full database schemas.
- **`examples/stationary.py`**: Example demonstrating stationary points, stages, and steps with H2O geometries.
- **`examples/scan.py`**: Example showing PES scan workflow with trajectory data.
- **`examples/transition.py`**: Example illustrating transition state and reaction pathway modeling.
- **`tests/test_events.py`**: Comprehensive test suite for SQLAlchemy ORM event listeners.
- **Top-level exports**: Added `CalculationTrajectoryLink`, `GeometryTrajectoryLink`, and `events` module to `autostorage.__all__`.

### Changed

- **Module consolidation**: Collapsed `src/autostorage/models/` directory (with separate `calc.py`, `data.py`, `geom.py`, `link.py`, `rxn.py`, `traj.py` files) into a single `src/autostorage/models.py` file — all row models, link tables, and helpers now live in one module, eliminating circular import complexity while maintaining logical organization via internal sections (link tables, existential data, stationary points, reaction network, identities).
- **Documentation reorganization**: Streamlined Sphinx docs — consolidated data model, development, and events pages into focused `models.md` and updated `database.md`; refreshed `index.md` and `quickstart.md` to reflect current architecture.
- **Examples refactor**: Replaced single `stationary_point.py` example with three focused examples (`stationary.py`, `scan.py`, `transition.py`), each demonstrating a distinct workflow pattern.
- **Test simplification**: Removed `tests/conftest.py` and test data files (`tests/data/*.xyz`, `*.gz`) — tests now use inline synthetic data instead of external fixtures.

### Removed

- **Alembic migrations** (`migrations/`, `alembic.ini`, `tests/test_migrations.py`, `pixi run migrate`): Removed for now; `alembic` remains a dev dependency for when migrations are reintroduced. Schema for fresh/in-memory `Database` instances is unaffected, still built via `create_all()`.
- **`src/autostorage/exc.py`**: Custom exception classes (`DataIntegrityError`, `ResultShapeError`) removed — validation errors now use standard Python exceptions or SQLAlchemy's built-in constraint violations.
- **`CalcType` and `CalcStatus` enums**: Removed from `types.py` and top-level exports — calculation type/status classification deferred to workflow layer or removed entirely.
- **Documentation files**: Deleted `docs/source/data-model.md`, `docs/source/development.md`, and `docs/source/events.md` (content consolidated into `models.md` and other updated docs).
- **`examples/stationary_point.py`**: Superseded by the three new focused examples.

## [0.0.12] - 2026-07-23
### Added
Expand Down
89 changes: 48 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Install as a [Pixi](https://pixi.sh) dependency:

```toml
[dependencies]
autostorage = ">=0.0.10"
autostorage = ">=0.0.12"
```

Or with `uv`/`pip` from PyPI:
Expand All @@ -31,7 +31,6 @@ Requires Python ≥3.12.
```python
import numpy as np
from autostorage import (
CalcType,
CalculationGeometryLink,
CalculationRow,
Database,
Expand All @@ -44,51 +43,59 @@ from autostorage import (
# Open (or create) a SQLite database; ":memory:" also works for scratch use.
db = Database("workflow.db")

# `find_or_create` dedups on (program, program_version, method, basis).
model = ModelRow.find_or_create(db, program="orca", method="b3lyp", basis="def2-svp")

calc = CalculationRow(model=model, calc_type=CalcType.ENERGY)
geo = GeometryRow(
symbols=["H", "O", "H"],
coordinates=np.array([[0, 0, 0.8], [0, 0, 0], [0.8, 0, 0]]),
charge=0,
spin=0,
)
link = CalculationGeometryLink.create(calc, geo, role=Role.INPUT)
db.add_all([model, calc, geo, link])
db.commit()

# Attach a result to the geometry/calculation pair.
energy = EnergyRow(geometry=geo, calculation=calc, value=-76.02)
db.add(energy)
db.commit()

# Look the result back up by geometry, model, and input provenance.
found = EnergyRow.query(db, geo=geo, model=model)
assert found is not None
print(found.value)
# Work within a session context.
with db.session() as session:
# Create a model specifying the calculation type, program, method, and basis.
model = ModelRow(
calc_type="energy",
program="orca",
method="b3lyp",
basis="def2-svp",
)

# Create a calculation using this model.
calc = CalculationRow(model=model)

# Create a geometry.
geo = GeometryRow(
symbols=["H", "O", "H"],
coordinates=np.array([[0, 0, 0.8], [0, 0, 0], [0.8, 0, 0]]),
charge=0,
spin=0,
)

# Link the geometry to the calculation as an input.
link = CalculationGeometryLink(
calculation=calc,
geometry=geo,
role=Role.INPUT,
)

# Add all objects to the session and commit.
session.add_all([model, calc, geo, link])
session.commit()

# Attach an energy result to the geometry/calculation pair.
energy = EnergyRow(geometry=geo, calculation=calc, value=-76.02)
session.add(energy)
session.commit()

# Query the result back by filtering on geometry and calculation.
found = session.query(EnergyRow).filter_by(
geometry_id=geo.id,
calculation_id=calc.id,
).first()
assert found is not None
print(found.value)

db.close()
```

`Database` also supports the `with` statement, which rolls back on an unhandled exception and closes the connection on exit:

```python
with Database("workflow.db") as db:
...
```

Two databases can be combined with `Database.merge_from()`, which copies every row from one into the other, remapping ids/foreign keys and deduplicating content-unique rows (models, non-auto-managed identities) against the target's existing data:

```python
with Database("combined.db") as target, Database("other.db") as other:
report = target.merge_from(other)
print(report.copied, report.reused) # per-table row counts
```
`Database.session()` returns a standard SQLAlchemy `Session` that supports the context manager protocol. Sessions don't commit automatically — call `session.commit()` explicitly to persist changes.

For a full worked example covering geometries, trajectories, results, and identities, see [`examples/stationary_point.py`](examples/stationary_point.py). Reaction networks (`StageRow`/`StepRow`) can be exported as [MESS](https://tcg.cse.anl.gov/papr/codes/mess.html) input via `autostorage.utils.export_mess_input()`, or rendered as a potential energy surface diagram via `autostorage.utils.plot_pes()`.
For a full worked example covering geometries, trajectories, results, stationary points, stages, and steps, see [`examples/stationary.py`](examples/stationary.py).

See `CLAUDE.md` for the full module map and architecture notes.
See [CLAUDE.md](.claude/CLAUDE.md) for the full module map and architecture notes, or the [Sphinx docs](docs/source) for a rendered quickstart and API reference.

## Contributing

Expand Down
Loading