Adapters and inference models
Sometimes a source port can’t feed a target port directly, but a conversion step can bridge the gap. The standard describes two kinds of step:
- Adapters convert data from one form to another, such as nM to µM.
- Inference models estimate data the source doesn’t contain, such as predicting gene expression from a genotype.
A plan is a chain of these steps that connects a source to a target. Every step appears as a named node in the plan, so you can see exactly what happens to the data between two models.
Adapters
An adapter changes how data is written without adding scientific assumptions. Examples:
- converting a concentration from nM to µM
- reordering features to match the target’s gene order
- mapping gene identifiers from one namespace to another, where every identifier has a match
An adapter capability is a JSON description of one adapter. It declares:
sourceandtarget: the contracts it converts from and totransformation_class:representation,unit,identifier,normalization,projectionoraggregationinformation_loss:none,boundedorlossystate:reviewedorrevokedref,sha256andrelease: which exact version of the adapter this is
It can also list preconditions, evidence, limitations, a loss_score and an execution_cost.
The hosted resolver only uses capabilities that an administrator has reviewed and added to the Biosimulant registry. A registry entry must include evidence and review notes. Sending a capability in an API request does not make it trusted: it must match an active registry entry exactly.
Inference models
An inference model estimates something the source doesn’t measure. Predicting gene expression from a genotype is inference, not an adapter: the result depends on the model’s assumptions and comes with uncertainty.
An inference capability has the same identity fields as an adapter, and must also declare inferred_modality, assumptions and uncertainty. By default, a plan that includes an inference step needs approval.
Example: nM to µM
The source port outputs a concentration in nM. The target port expects µM:
# source contract
measurement:
unit: nM
# target contract
measurement:
unit: uMComparing the two contracts on their own returns LOSSLESS_CONVERSION_AVAILABLE: the standard’s unit table knows the conversion. To get a plan, you also need an adapter that does the conversion. Here is its capability:
{
"schema_version": "0.1",
"ref": "https://example.org/adapters/nm-to-um/v1",
"sha256": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"source": { "measurement": { "unit": "nM" } },
"target": { "measurement": { "unit": "uM" } },
"state": "reviewed",
"transformation_class": "unit",
"information_loss": "none",
"evidence": [
{
"kind": "test-report",
"ref": "https://example.org/adapters/nm-to-um/v1/evidence"
}
],
"release": { "version": "1.0.0" }
}The ref and sha256 here are placeholders. A real capability uses its own URL and the sha256 of its definition.
When you resolve the source and target with this capability, the result is RESOLVED and the plan has three nodes:
{
"nodes": [
{ "id": "source", "kind": "contract", "contract_digest": "sha256:bd0668d7…" },
{ "id": "adapter-1", "kind": "adapter", "ref": "https://example.org/adapters/nm-to-um/v1", "information_loss": "none" },
{ "id": "target", "kind": "contract", "contract_digest": "sha256:1a3f61a2…" }
],
"edges": [
{ "from": "source", "to": "adapter-1" },
{ "from": "adapter-1", "to": "target" }
],
"policy": { "decision": "ALLOW" }
}The adapter loses no information, so the default decision is ALLOW. If you resolve without any capabilities, the result is UNRESOLVED with reason NO_CAPABILITY_PATH: the units can be converted, but nothing was offered to do the conversion.
If the step were an inference model instead, for example one that predicts expression from genotype, the plan would contain an inference node and the decision would be APPROVAL_REQUIRED.
You can resolve contracts with biosimulant compatibility plan or the /v1/compatibility/resolve endpoint. See API and CLI.
How the resolver picks a plan
When more than one plan connects the same ports, the resolver prefers, in this order:
- Fewer inference steps
- Less information loss, judged by the worst step (
none, thenbounded, thenlossy) - A lower total
loss_score - Fewer steps
- A lower total
execution_cost
If two or more plans tie on all five, the resolver doesn’t choose. It returns AMBIGUOUS with the tied plans, sorted by capability ref and sha256, and you pick one.
The search has limits. A plan can have at most 8 steps and at most 2 inference steps. The resolver checks at most 10,000 capabilities in total, and it never passes through the same contract twice in one plan. If it hits the check limit without finding a plan, it returns UNRESOLVED with reason SEARCH_LIMIT_EXCEEDED.
The Python and TypeScript resolvers both check capability preconditions. A precondition can only use the safe rule operators in this standard. If the resolver cannot prove a precondition, it does not use that capability.
Rules that depend on an ontology or identifier mapping need an exact snapshot. The request supplies the snapshot’s ref, sha256 and data. The report and plan record that reference, so the same decision can be checked later.
Revoked capabilities
The resolver only uses capabilities whose state is reviewed. A revoked capability never ends up in a new plan. Each plan records the ref and sha256 of every capability it uses. The hosted service also marks unused plans invalid when a capability in the plan is revoked. Completed run records stay available as historical evidence.