Skip to content

omnilab/cod

Omnilab COD: a crystal structure of the Crystallography Open Database by its COD id (9008565) - its names, formula, unit cell with uncertainties, space group, Z, temperature, radiation, R factors, publication and DOI - the entry it duplicates as a related thing, its CIF as a file; a search of the entries' metadata; no key.

Floor
Provider · Omnilab
Version
1.x
Conformance suite
not wired
Verified for real
Its README does not say
Licence
MIT
composer require omnilab/cod:1.x-dev

The Crystallography Open Database (https://www.crystallography.net/cod/, its result search endpoint) for omnisense/omnilab: crystal structures by COD id - formula, unit cell with its uncertainties, space group, Z, temperature, R factors, publication and DOI -, as entities; the CIF as a file; a search of the entries' metadata. No key.

Installation

composer require omnilab/cod
use Omnilab\Cod\CodSourceFactory;

$cod = (new CodSourceFactory($httpClient))->create([
    'mailto' => 'support@glitchr.io',  // optional
    'throttle' => 0.5,                 // optional: seconds between two calls
]);

Credentials

None: COD is open, no key, no account.

Options

The factory's options, with the defaults it applies (CodSourceFactory):

Option Type Default Required Effect
mailto ?string null no a contact address, sent in the User-Agent
user_agent ?string null no the whole User-Agent, in place of the one made from mailto
throttle float 0.5 no seconds left between two calls: COD publishes no rate, and answered 403 to eight calls in ten seconds
base_uri string https://www.crystallography.net/cod/ no COD's site (result and {id}.cif are joined to it), or a mirror's

Capabilities

capabilities() answers entity, search, open; an operation it does not answer throws NotSupportedException before any call.

Capability Supported
entity yes a COD id (seven digits, cod:, its page, its CIF's address), whatever COD flags it (duplicate, errors, theoretical); another scheme refused before any call
search yes every entry whose metadata hold the text (publication, compound's and mineral's names), 100 a page at most, two calls a page; total the number of ids
series no
products no
occurrences no
datasets no
jobs no
open yes a file of COD's site (a CIF, .hkl reflection data), streamed; another site's address refused before any call
structure no the structure is the CIF (cif()), not an image
cross_references no none: COD gives no other base's id; the entry it duplicates is related('duplicate_of')
forecast no
historical no
area no
station no
cloud_cover no

Outside the contract, on Omnilab\Cod\CodSource: cif(Identifier|string $id): Asset, the CIF of a structure as a file to download (chemical/x-cif, role data), without a call; open() reads it.

The scheme

A structure is named in the core's cod scheme by its seven digits (1000000), read from cod:1000000, its page crystallography.net/cod/1000000.html and its CIF's address. Wikidata keeps COD ids in P9824 (on 1,599 items on 2026-10-15), which omnilab/wikidata reads as a related thing (related('structure'): a compound's structures are not the compound), so the Collector does not follow them into COD; $cod->entity($item->related('structure')->value(Scheme::COD)) reads one.

Which API

COD's search endpoint crystallography.net/cod/result (its parameters on the COD wiki's "RESTful API" page):

Method Calls
entity() result?id={id}&format=json&include_duplicates=1&include_errors=1&include_theoretical=1: the row of COD's database for that id, whatever COD flags it (a duplicate, an entry with errors, a theoretical structure: COD's search leaves them out otherwise, and 1000000 is a duplicate)
search() result?text={text}&format=lst (every id whose metadata hold the text: publication, compound's and mineral's names), cut into pages here, then result?id={ids}&format=json&... for the page's rows: two calls a page; total is the number of ids
cif() no call: the CIF's address, crystallography.net/cod/{id}.cif (outside the contract)
open() that address (or any of COD's), streamed

An unknown id is an empty list (null); a text nothing matches an empty answer (an empty page); an error page a ProviderException with its title.

What an entity holds

id (cod:9008565), label (the mineral's name, else the common name, else the systematic name, else the formula), synonyms (the common name when the label is the mineral's), domain chemistry, kind crystal structure, url (its page), and:

Key COD Value
chemical_formula, calculated_formula, cell_formula formula, calcformula, cellformula Hill order, spaced as COD writes it: C5 H17 Al N2 O8 P2
elements nel how many distinct elements
mineral, systematic_name mineral, chemname text
space_group, space_group_hall, space_group_number sg, sgHall, sgNumber F d -3 m :1, F 4d 2 3 -1d, 227
cell_a, cell_b, cell_c a, b, c (siga...) Å, with its uncertainty
cell_alpha, cell_beta, cell_gamma alpha, beta, gamma °, with its uncertainty
cell_volume vol (sigvol) ų
z, z_prime Z, Zprime numbers
cell_temperature, diffraction_temperature celltemp, diffrtemp K
cell_pressure, diffraction_pressure cellpressure, diffrpressure kPa (CIF's unit)
method, radiation, wavelength method, radType, wavelength text, Å
r_obs, r_all Robs, Rall numbers
title, authors, journal, year, volume, issue, pages, doi the publication text, year a number
flag (one each), status, revised flags, status, date has coordinates...
cif - the CIF's address

identifiers: its own alone. related: duplicate_of (the COD entry it duplicates). COD gives no identifier of another base (no InChIKey, no CAS): the Collector has nothing to follow.

Example

<?php

require __DIR__.'/vendor/autoload.php';

use Omnilab\Cod\CodSourceFactory;
use Omnilab\Source\Query;
use Symfony\Component\HttpClient\HttpClient;

$cod = (new CodSourceFactory(HttpClient::create()))->create(['mailto' => 'you@example.org']);

$silicon = $cod->entity('cod:9008565');
echo $silicon->label, ' (', $silicon->id, '): ', $silicon->value('space_group'), ', a = ', $silicon->property('cell_a'), ', ', $silicon->value('journal'), ' ', $silicon->value('year'), "\n";

$cif = $cod->cif('9008565');
echo 'CIF ', $cif->href, ': ', strlen($cod->open($cif)->getContent()), " bytes\n";

$page = $cod->search(new Query('caffeine', limit: 3));
printf("%d entries mention caffeine; the first %d:\n", $page->total, count($page));
foreach ($page as $entry) {
    echo '  ', $entry->id, '  ', $entry->label, '  ', $entry->value('space_group'), "\n";
}
Silicon (cod:9008565): F d -3 m :1, a = 5.4307 Å, Crystal Structures 1963
CIF https://www.crystallography.net/cod/9008565.cif: 5110 bytes
117 entries mention caffeine; the first 3:
  cod:1505182  caffeine 1-hydroxy-2-naphthoic acid  P 1 21/n 1
  cod:1505183  caffeine 3-hydroxy-2-naphthoic acid  P 1 21/c 1
  cod:1505184  C19 H18 N4 O5  P -1

(as answered on 2026-10-27: four calls, half a second apart)

# config/packages/omnilab.yaml
omnilab:
    sources:
        cod: { factory: cod, options: { mailto: '%env(OMNILAB_MAILTO)%', throttle: 0.5 } }

cif() is a method of CodSource, not of the interface the bundle injects: check the instance before calling it.

Errors

Every call goes through HttpSource: a 404 or a 410 (and a 204) is null - an empty page for a list -; a 429 is a RateLimitedException (retryAfter from Retry-After); a 5xx, a time-out or a host that does not answer an UnavailableException; any other 4xx a ProviderException with its status and what the service said; an answer that is not JSON a ProviderException, a web page answered for JSON an UnavailableException.

Before any call: an identifier of another scheme given to entity() or cif(), and open() of another site's address (NotSupportedException). An unknown id is an empty list, null; a text nothing matches an empty page; COD's error page a ProviderException with its title, its 403 past its rate a ProviderException with status 403 (not a RateLimitedException: COD does not answer 429). open() makes its call directly: neither the throttle nor the status mapping apply to it, the response's status is the caller's to check.

Verified, and not

Against the real service on 2026-10-15: silicon (9008565), the aluminophosphate 1000000 (a duplicate of 2011331, its cell's uncertainties, its DOI), an unknown id, the search for "caffeine" (117 entries) in two pages of three, a text nothing matches, silicon's CIF. The fixtures in Tests/Fixtures/ are those answers (the CIF cut to its head). COD's server was slow and uneven that day: a call took 0.3 to 7.6 s, a few connections were dropped without an answer, and a run of eight calls in ten seconds got a 403 ("Forbidden") on the eighth - hence the default throttle of 0.5 s. Again on 2026-10-27: silicon, its CIF (5,110 bytes), the search for "caffeine" (117 entries; the example above). Not verified: COD's mirrors; reflection data (.hkl); the other search parameters (formula, elements, cell ranges), which this package does not use yet.

Responsibility

This package is an independent, unofficial client: it is not affiliated with, endorsed by or sponsored by the Crystallography Open Database. COD and its data are dedicated to the public domain (CC0); its users acknowledge the original authors of the structural data (each entry's publication is in its properties) and cite COD as its "Citing COD" page says, as the README's Terms of use says. Whoever runs the package is responsible for any use made of what it returns: see the README's Responsibility section.

Propose a change docs/index.md