<!-- Source: https://docs.biosimulant.com/standards/model-compatibility/adapters-and-inference -->

# 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:

- `source` and `target`: the contracts it converts from and to
- `transformation_class`: `representation`, `unit`, `identifier`, `normalization`, `projection` or `aggregation`
- `information_loss`: `none`, `bounded` or `lossy`
- `state`: `reviewed` or `revoked`
- `ref`, `sha256` and `release`: 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:

```yaml
# source contract
measurement:
  unit: nM

# target contract
measurement:
  unit: uM
```

Comparing 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:

```json
{
  "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:

```json
{
  "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](/standards/model-compatibility/api-and-cli).

## How the resolver picks a plan

When more than one plan connects the same ports, the resolver prefers, in this order:

1. Fewer inference steps
2. Less information loss, judged by the worst step (`none`, then `bounded`, then `lossy`)
3. A lower total `loss_score`
4. Fewer steps
5. 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.
