TCKDB scientific query cookbook
A practical, recipe-style guide to querying TCKDB as a hosted
scientific information system. Every example uses tckdb-client
directly — no ARC-specific wrappers, no integer-ID assumptions, no
client-side ranking.
New to hosted TCKDB querying? Start with public_hosted_querying.md for the entry-point overview (anonymous reads, refs, abuse-control expectations) before working through the recipes here.
The corresponding runnable script is
clients/python/examples/query_cookbook.py.
Each recipe in this guide maps 1:1 to a function named
recipe_<name> in that script — pick one, lift it into your code,
and it should work.
python examples/query_cookbook.py --recipe list
python examples/query_cookbook.py --recipe lowest_sp_energy --smiles "O"
python examples/query_cookbook.py --recipe all --smiles "O" --json
Operating principles
- Refs are the normal hosted handles. Every response carries a
*_reffield next to anywhere an integer id once lived. Use the ref to chain follow-up reads. - Integer ids are compatibility / debug fields. They are hidden
by default. Pass
include=["internal_ids"]and run against a deployment that setsALLOW_PUBLIC_INTERNAL_IDS=trueto see them again (see internal-ids policy). - Hosted search is chemistry-first. Callers ask about SMILES, reactants/products, level of theory — not table primary keys. The backend returns the resolved identity (ref) along with the scientific payload, so chaining is "free."
- No client-side ranking. Every method passes
ranking=,min_review_status=, etc. straight to the backend. No read takescollapse: a read lists every matching record, and "give me one" is theselect_*methods. The backend's documented sort order is the source of truth.
Setup
from tckdb_client import TCKDBClient
client = TCKDBClient(
base_url="http://127.0.0.1:8010/api/v1", # include /api/v1
api_key=None, # optional for public scientific reads
timeout=30.0,
)
api_key is optional for public scientific reads. Use it only when the
deployment requires authentication or when you need authenticated
quotas/features. If an api_key is set, the client forwards it on every
request (including reads), so authenticated deployments still see a
billable identity.
Public reads being anonymous-friendly in the client is not an abuse-control mechanism. Hosted deployments should enforce abuse limits server-side through rate limits, pagination caps, query timeouts, and monitoring.
Use as a context manager when convenient: with TCKDBClient(...) as client: — that closes the underlying HTTP connection at scope exit.
Recipe 1 — Species search by SMILES
Q: What species does TCKDB have that match this SMILES?
species = client.search_species(
smiles="O",
include=["review"],
)
for record in species["records"]:
print(record["species_ref"], record["canonical_smiles"])
for entry in record["entries"]:
print(
" ", entry["species_entry_ref"],
"review:", entry["review"]["status"],
"availability:", entry["availability"],
)
What you get back:
records[*].species_ref— public handle for the chemical species (one per identity).records[*].entries[*].species_entry_ref— handle for one physical species entry (a specific charge / multiplicity / electronic state realization). This is the handle every other species-side endpoint takes.records[*].entries[*].availability— boolean flags + counts so the caller can see at a glance whether thermo / statmech / conformers / calculations exist.records[*].entries[*].species_entry_label— the short discriminator that says which entry this is:"E","Z","excited T1", ornullfor the plain ground-state, stereo-unlabelled entry. Built fromstereo_label,electronic_state_kind,electronic_state_label,term_symbolandisotope_key, which are also served individually if you need them machine-readable.records[*].stereo_kind— the species' own:achiralmeans anullstereo label has nothing to label;ez_isomerorenantiomermeans stereoisomers exist and this entry simply is not labelled.
Common gotchas:
- Multiple
entriesper species is the normal case (e.g. ground-state vs excited-state variants of the same molecule). canonical_smilesdoes not identify an entry. It is the species' graph identity and is shared by every entry beneath it — the deployedN=Ncarries aZentry (cis-diazene) and anEentry (trans-diazene) with different thermochemistry, and both readN=N. Pick an entry byspecies_entry_label(or the identity fields), never by position inentriesand never by the species-level fields.inchicurrently has no enforced search path, so supplying it returns 422unsupported_filter; usesmiles,inchi_key,formula(derived through RDKit), or a public ref.
Recipe 2 — Thermo search by species
Q: Give me the first thermo record for this species (in display order), and tell me whether it covers the temperature window I care about.
thermo = client.search_thermo(
smiles="O",
coverage_temperature_min_k=300,
coverage_temperature_max_k=2000,
include=["provenance", "review"],
)
rec = thermo["records"][0]
# species_entry_label says which entry of the species this number is for --
# two stereoisomers share canonical_smiles, so it is the only thing in the
# species block that tells them apart in prose.
print(rec["species"]["species_entry_ref"], rec["species"]["species_entry_label"])
print(rec["species"]["species_entry_ref"], rec["thermo"]["thermo_ref"])
print("H298 =", rec["thermo"]["h298_kj_mol"], "kJ/mol")
print("S298 =", rec["thermo"]["s298_j_mol_k"], "J/mol/K")
The backend's locked display order for thermo (review_rank ASC, created_at DESC, id DESC: approved first, under_review and not_reviewed together, then newest deposited; see Browse order) decides which record is listed first. It is the same order the export, CHEMKIN and ML export list records in, so the website and an export list the same record first. No read or export picks one: for that, use select_species_thermo. That is display order, not a scientific preference: TCKDB does not rank one method above another. Origin does not matter: a newer experimental record is listed before an older computed one with the same review status. The client does not re-rank.
Useful fields:
records[*].thermo.model_kind—nasa(polynomial coefficients inrecords[*].thermo.nasa),points(inrecords[*].thermo.points), orscalar(justh298/s298).records[*].thermo.temperature_coverage—covers_requested_range,extrapolation_distance_k. Reported per record.coverage_temperature_min_k/coverage_temperature_max_kdo not filter and do not affect which record is first: display order is review status, then newest. Check this field yourself if you need a record that spans your window (a scalar record has no range and reportscovers_requested_range: false).records[*].thermo.evidence_completeness— score + per-predicate checklist. Displayed only, not part of the order. Most predicates are calculation traceability, so an experimental record scores low by construction; read it together withscientific_origin.
Recipe 3 — Thermo provenance inspection
Q: What freq / SP calculations stand behind this thermo record?
rec = thermo["records"][0]
prov = rec["thermo"]["provenance"]
print("statmech_ref :", prov["statmech_ref"])
print("freq_calculation_ref :", prov["freq_calculation_ref"])
print("sp_calculation_ref :", prov["sp_calculation_ref"])
lot = prov["level_of_theory"] or {}
sw = prov["software"] or {}
print("level_of_theory_ref :", lot.get("level_of_theory_ref"),
f"({lot.get('method')}/{lot.get('basis')})")
print("software_release_ref :", sw.get("software_release_ref"),
f"({sw.get('software')} {sw.get('version')})")
Important detail:
- For computed thermo linked to a statmech (
thermo.statmech_id), the read service uses that statmech's source calculations for the roles the thermo itself didn't declare. Sofreq_calculation_refandsp_calculation_refare populated for a linked thermo with noThermoSourceCalculationrows of its own. - Explicit thermo source calcs always take precedence over the linked statmech's.
- A thermo with no
statmech_idand no source calcs showsstatmech_ref, calculation refs and level of theory as null. Nothing is borrowed from other statmech records on the species entry, and the evidence score counts only the record's own links.
Recipe 4 — Kinetics search by reactants/products
Q: Find kinetics for this elementary reaction.
kinetics = client.search_kinetics(
reactants=["[CH3]", "[CH3]"],
products=["CC"],
direction="either",
coverage_temperature_min_k=300,
coverage_temperature_max_k=2000,
include=["provenance", "review"],
)
rec = kinetics["records"][0]
reaction = rec["reaction"]
params = rec["kinetics"]["parameters"]
print("reaction_entry_ref:", reaction["reaction_entry_ref"])
print("matched_direction :", reaction["matched_direction"])
print("kinetics_ref :", rec["kinetics"]["kinetics_ref"])
print(f"A = {params['A']} ({params['A_units']}) n = {params['n']}"
f" Ea = {params['Ea_kj_mol']} kJ/mol")
Notes:
direction="either"matches in either forward or reverse orientation; the response says which (matched_direction).direction="exact"is not supported in v0 — the backend returns 422. It is not the same thing asmatch="exact", which is supported and described below.match="contains"(the default) means set containment per role: every species you name must appear in that role, and a side you leave out constrains nothing. Sosearch_kinetics(reactants=["CC"])on its own is a real query — "kinetics for reactions consuming ethane" — and so issearch_reactions(products=["[OH]"]). Counts are ignored in this mode: naming one[OH]matches a reaction consuming two.match="exact"demands the whole equation, both sides, counts included. Use it when you mean one specific reaction and do not hold itsreaction_ref. Beforematchexisted this was the only behaviour, which is why a one-sided query used to come back empty.- The first record in the list is first in display order (review status, then newest, then id); it is not a pick, and nothing is dropped.
coverage_temperature_min_k/coverage_temperature_max_kdo not filter or re-rank; they only fill each record'stemperature_coverage, andevidence_completenessis displayed, not ranked. - Non-TS-backed kinetics (experimental, estimated, network-derived, …) come through with
provenance.transition_state_entry_ref = nulland everyts_*_calculation_ref = null. That's not an error — it's how the schema signals "no transition-state chain to follow."
Recipe 5 — Species-calculation search by species + level of theory
Q: What sp calculations exist for this species at any level of theory? (Or scope to one LoT.)
# All calculations of a given type for this species.
calcs = client.search_species_calculations(
smiles="O",
calculation_type="sp",
include=["provenance", "review"],
)
for rec in calcs["records"]:
calc = rec["calculation"]
lot = rec["level_of_theory"]
print(
calc["calculation_ref"],
calc["calculation_type"],
calc["calculation_quality"],
lot["method"], lot["basis"],
lot["level_of_theory_ref"],
)
Scope to one LoT (refs are the preferred filter):
calcs_at_lot = client.search_species_calculations(
smiles="O",
calculation_type="sp",
level_of_theory_ref="lot_…",
)
Both level_of_theory_id (compatibility) and level_of_theory_ref are accepted as filters. Supplying both and disagreeing returns 422 level_of_theory_handle_conflict.
Recipe 6 — Lowest single-point energy retrieval
Q: For this species, what's the lowest SP energy across all levels of theory in TCKDB?
lowest = client.search_species_calculations(
smiles="O",
calculation_type="sp",
ranking="lowest_energy",
include=["provenance", "review"],
)
rec = lowest["records"][0]
energy = rec["energy"]
lot = rec["level_of_theory"]
print(rec["calculation"]["calculation_ref"],
energy["energy_hartree"], energy["energy_kind"],
lot["method"], lot["basis"])
Rules:
ranking="lowest_energy"requirescalculation_type∈ {sp,opt} — otherwise the backend returns 422unsupported_ranking_for_calculation_type. The client doesn't validate; the error surfaces with a stable code.- For
sp, the energy comes fromCalculationSPResult.electronic_energy_hartree. Foropt, fromCalculationOptResult.final_energy_hartree. - Records whose energy column is null sort to the end (
NULLS LAST).
Recipe 7 — Optimized geometry retrieval
Q: For this species, give me an opt calculation and the ref of its optimized geometry.
opt = client.search_species_calculations(
smiles="O",
calculation_type="opt",
include=["provenance", "review"],
)
rec = opt["records"][0]
geom = rec["geometry"]
print("calculation_ref :", rec["calculation"]["calculation_ref"])
print("primary_output_geometry_ref:", geom["primary_output_geometry_ref"])
print("primary_output_geometry_role:", geom["primary_output_geometry_role"])
print("input_geometries :",
[g["geometry_ref"] for g in geom["input_geometries"]])
print("output_geometries :",
[g["geometry_ref"] for g in geom["output_geometries"]])
Geometry expectations by calculation type:
| Type | input_geometries |
output_geometries |
primary_output_geometry_ref |
|---|---|---|---|
opt |
populated | auto-attached final role (if upload omitted) |
populated |
sp |
populated | empty by design | null by design |
freq |
populated | producer-declared only | producer-declared only |
scan / irc / path_search |
producer-declared | producer-declared (per-point) | producer-declared |
Don't assume an SP has an output geometry — that's the schema speaking.
Recipe 8 — Geometry coordinate download by geometry_ref
Q: I have a geom_… ref. Give me the atoms.
geometry = client.get_geometry("geom_…", include=["provenance"])
print("geometry_ref :", geometry["geometry_ref"])
print("natoms :", geometry["natoms"])
print("format / units :", geometry["format"], "/", geometry["coordinate_units"])
print("symbols :", geometry["symbols"])
print("coords (Ångström) :")
for sym, (x, y, z) in zip(geometry["symbols"], geometry["coords"]):
print(f" {sym:<2} {x:>10.3f} {y:>10.3f} {z:>10.3f}")
prov = geometry["provenance"]
print("produced_by :",
[(p["calculation_ref"], p["role"]) for p in prov["produced_by"]])
print("used_as_input_by :",
[p["calculation_ref"] for p in prov["used_as_input_by"]])
Notes:
- The path parameter accepts either the integer
geometry.id(compatibility) or ageom_…public ref. A wrong-prefix ref returns 422; an unknown ref returns 404. - The
provenanceblock lists every calculation that produced this geometry (withrole) and every calculation that consumed it (no role — input links have no role column in v0). Useful for tracing why a geometry exists.
Recipe 9 — Ref-based follow-up reads (chaining)
Q: How do hosted workflows compose multiple reads safely?
The shape of every hosted workflow is:
chemistry → search_*(…) → ref → get_*(ref)
Concrete example: SMILES → thermo search → species-entry ref → entry-detail thermo read.
summary = client.search_thermo(
smiles="O",
coverage_temperature_min_k=300, coverage_temperature_max_k=2000,
include=["provenance", "review"],
)
species_entry_ref = summary["records"][0]["species"]["species_entry_ref"]
detail = client.get_species_thermo(
species_entry_id=species_entry_ref, # ref accepted on the path
coverage_temperature_min_k=300, coverage_temperature_max_k=2000,
include=["provenance", "review"],
)
Other ref-based handles you'll commonly see in responses:
| Where it appears | Ref name | Used by |
|---|---|---|
search_thermo() / search_species_calculations() |
species_entry_ref |
get_species_thermo(species_entry_id=<ref>) |
search_kinetics() / search_reactions() |
reaction_entry_ref |
get_reaction_kinetics(reaction_entry_id=<ref>) and get_reaction_full(reaction_entry_id=<ref>) |
search_species_calculations() |
level_of_theory_ref |
filter on subsequent search_species_calculations(level_of_theory_ref=<ref>) |
search_species_calculations() (opt records) |
primary_output_geometry_ref |
get_geometry(<ref>) |
search_species_calculations() (sp records) |
input_geometries[*].geometry_ref |
get_geometry(<ref>) |
The detail endpoints (get_species_thermo, get_reaction_kinetics, get_reaction_full) all accept either the integer PK or a public ref as the path parameter, and supplying both *_id and *_ref in query filters validates consistency (mismatch → 422 <resource>_handle_conflict).
Recipe 10 — Optional: requesting integer IDs (local / debug)
Q: I'm running TCKDB locally and want to see integer IDs for debugging.
response = client.search_species(
smiles="O",
include=["review", "internal_ids"],
)
print("request echo include:", response["request"]["include"])
rec = response["records"][0]
print("species_ref:", rec["species_ref"])
print("species_id :", rec.get("species_id", "(hidden)"))
How the token resolves:
ALLOW_PUBLIC_INTERNAL_IDS=False(hosted production default):internal_idsis silently dropped from the resolved include set. It does not appear inresponse["request"]["include"], and the response keeps the refs-only shape. No 4xx.ALLOW_PUBLIC_INTERNAL_IDS=True(local / dev / explicitly enabled):internal_idsappears in the echo and integer*_idfields are restored everywhere they exist — including in nested provenance summaries, on geometry detail responses, and inside the/fullaudit array.
Two related rules:
include=alldoes not expand tointernal_ids. If you genuinely want everything, passinclude=["all", "internal_ids"].- Caller-supplied integer-id filters (
level_of_theory_id=8,species_entry_id=31, …) are always echoed back inrequest.filterregardless of the visibility setting — request echoes mirror caller input. The visibility rule applies only to ids the backend resolved internally (e.g. an integer behind a ref) which are never leaked into the echo.
Cross-references
- Workflow-tool integration guide — the long-form prose tour of the read API.
- Public identifier policy — the design behind the
*_refscheme. - Internal-ID visibility policy — Phase D contract for the
internal_idsopt-in. - Scientific read demo data — the local seed dataset you can hit with every recipe in this cookbook.