Skip to content

Canonical identifiers

For the identifier helper functions and their exact behavior, see the model reference.

Every scholarcore entity has exactly one join key:

EntityIdentifierAssigned by
PersonridORCID, or minted locally
Papernormalized DOI, or PMIDthe publisher / PubMed
Awardapplication_idthe system of record tracking the application
Opportunityopportunity_numberthe funding agency

Two systems that both store the same identifier can join their records without sharing a database, and without either system knowing the other exists.

A researcher id is either a canonical ORCID or an explicitly-prefixed local id:

0000-0002-1825-0097 an ORCID
local:josiah-carberry-a3f19c a locally-minted id

ORCIDs are validated by pattern and by their ISO 7064 MOD 11-2 check digit, so a transposed digit is rejected rather than stored as a plausible-looking id for a different person.

There is no separate orcid field. An ORCID is derived from the rid with orcid_of(), which returns None for a local id. A single field cannot drift out of sync with itself.

Most researchers in a real dataset have no ORCID, so mint_local_rid() builds one from a display name plus six random hex characters:

from scholarcore import mint_local_rid
mint_local_rid("Josiah Carberry") # local:josiah-carberry-a3f19c

The hex suffix is generated once and stored. It is what keeps the id stable when a person changes their name, and what keeps two people with the same name apart. Minting a second local id for a person who already has one creates a second identity for the same human.

The local: prefix contains a colon and lowercase letters, so it can never be mistaken for an ORCID, whose pattern allows only digits and hyphens.

The same DOI is written many ways in practice:

10.1101/gr.example
https://doi.org/10.1101/gr.example
http://dx.doi.org/10.1101/gr.example
doi:10.1101/gr.example

Paper.doi normalizes on load, with any resolver prefix and scheme stripped, so those forms collapse to one written form. Case is preserved: some sources (OpenAlex among them) carry the publisher’s original case, and consumers depend on it. DOIs are case-insensitive, so compare with .lower(), or call normalize_doi(value, lowercase=True), when using one as a join key. PMIDs normalize the same way, to bare digits.

Normalization happens in the model rather than in each consumer because a join key that every caller has to remember to normalize is a join key that eventually fails to match. A PMID carrying no digits loads as None instead of raising, so one malformed record does not fail an entire import.

An award has several plausible keys: a grant number, an internal slug, a funder’s tracking number. application_id is the one scholarcore treats as canonical, because it is the identifier already shared between the systems that track a grant through its lifecycle. Award.number carries the award or grant number separately.

The funder-assigned number for a published call, such as PA-25-168. It is assigned by the agency, appears in the announcement, and is what applicants quote, so no local identifier is needed.

Organization and Affiliation have no canonical identifier of their own. Organizations carry an optional ror_id from the Research Organization Registry, which is the closest thing to a canonical key, but many organizations and nearly all departments have none, so an organization is identified by whatever fields a record happens to carry.