pyadi-jif to pyadi-dt interface contract
Status
This page defines the proposed, versioned agreement between pyadi-jif and
pyadi-dt. The executable pyadi-dt reference model is
adidt.jif_contract and its focused tests are in
test/test_jif_contract.py.
The first contract version is intentionally introduced alongside the existing raw configuration paths. Pipeline and CLI integration can migrate in later, reviewable changes without changing the contract’s ownership boundary.
Why a contract is needed
pyadi-jif currently returns a solver-oriented dictionary. Keys can be assembled
from component names, such as jesd_AD9081_RX, while clock outputs combine
semantic names with positions in out_dividers. Existing pyadi-dt flows consume
variants of that result by copying selected JESD keys, indexing divider lists,
or looking up implementation-specific names.
Those dictionaries are useful inside pyadi-jif, but they are not a safe public interface. In particular, a requested-clock position in pyadi-jif does not identify a physical HMC7044 or AD952x output. Physical placement is board wiring and therefore belongs to pyadi-dt.
Contract decision
Use a strict, versioned JSON interchange document with typed projections in both projects.
Contract name:
adi.jif-dtInitial version:
1.0Units: integer hertz, named with an
_hzsuffixIDs: stable lowercase semantic IDs, independent of Python class names and DT labels
Compatibility: consumers reject unsupported major versions and unknown 1.0 fields
Safety: parse, validate, bind, and check topology before rendering or changing a device tree
The JSON shape is the public API. Neither project’s internal class hierarchy is part of the agreement. A saved contract can therefore be consumed without installing a solver backend.
Ownership boundary
pyadi-jif owns solved electrical intent
pyadi-jif produces:
JESD standard, direction, framing, sample rate, and lane rate;
semantic source/sink clock requirements and solved rates/dividers;
FPGA transceiver or PLL results needed to implement a link;
producer and solver provenance.
pyadi-dt owns physical topology
pyadi-dt supplies:
XSA-discovered instances and direction;
DT labels, compatible strings, phandles, and JESD link IDs;
physical clock-output channel indices;
SPI buses and chip selects, GPIOs, interrupts, and DMA labels;
board/profile defaults and final DTS rendering.
Values that must not cross the boundary
The contract excludes solver expressions and backend objects, arbitrary DT
properties, SPI/GPIO placement, implicit units, and positional
out_dividers treated as physical channels.
Interchange example
{
"schema": "adi.jif-dt",
"version": "1.0",
"producer": {
"name": "pyadi-jif",
"version": "0.1.6"
},
"jesd_links": [
{
"id": "ad9680.rx",
"direction": "adc-to-fpga",
"converter": "AD9680",
"fpga": "zc706",
"standard": "jesd204b",
"sample_rate_hz": 1000000000,
"lane_rate_hz": 10000000000,
"parameters": {
"F": 1,
"K": 32,
"L": 4,
"M": 2,
"N": 16,
"Np": 16,
"S": 1,
"HD": 0,
"CS": 0,
"CF": 0
},
"fpga_config": {
"type": "qpll",
"sys_clk_select": "XCVR_QPLL0"
}
}
],
"clock_requirements": [
{
"id": "ad9680.device-clock",
"role": "converter-device",
"sink": "AD9680",
"source": "HMC7044",
"rate_hz": 1000000000,
"divider": 3
},
{
"id": "ad9680.sysref",
"role": "converter-sysref",
"sink": "AD9680",
"source": "HMC7044",
"rate_hz": 7812500,
"divider": 384
}
],
"metadata": {
"solver": "CPLEX"
}
}
fpga_config and metadata are isolated extension points. A consumer must not
require arbitrary keys from either mapping without promoting those keys into a
later contract version.
Binding semantic intent to a board
JifDtContract validates the portable result.
JifDtBindings is a separate pyadi-dt-owned object
that maps semantic IDs to physical endpoints:
from adidt.jif_contract import (
ClockBinding,
JesdBinding,
JifDtBindings,
JifDtContract,
)
contract = JifDtContract.from_json_file("solved.jif-dt.json")
bindings = JifDtBindings(
clocks=(
ClockBinding(
requirement_id="ad9680.device-clock",
dt_label="hmc7044",
output_index=13,
),
ClockBinding(
requirement_id="ad9680.sysref",
dt_label="hmc7044",
output_index=5,
),
),
jesd_links=(
JesdBinding(
link_id="ad9680.rx",
converter_label="ad9680",
jesd_label="axi_ad9680_jesd204_rx",
xcvr_label="axi_ad9680_adxcvr",
),
),
)
bindings.check(contract)
The check is required before render or apply. Keeping bindings separate permits one electrical solution to be used with multiple carrier/profile placements without teaching pyadi-jif about DT labels.
Runnable examples
The examples under examples/jif_contract/ are executed by the test suite, so
the documented API and checked-in JSON cannot drift silently.
Consume a saved AD9680 handoff
This example loads solver output and board bindings from separate JSON files. It validates both documents and their one-to-one semantic join without importing pyadi-jif or a solver backend:
python examples/jif_contract/consume_ad9680.py
"""Load and bind a saved AD9680 pyadi-jif handoff without a solver import."""
from __future__ import annotations
import json
from pathlib import Path
from adidt.jif_contract import JifDtBindings, JifDtContract
EXAMPLE_DIR = Path(__file__).resolve().parent
def main() -> None:
"""Validate the portable contract, then bind every semantic endpoint."""
contract = JifDtContract.from_json_file(EXAMPLE_DIR / "ad9680.jif-dt.json")
bindings = JifDtBindings.model_validate_json(
(EXAMPLE_DIR / "ad9680.bindings.json").read_text()
)
bindings.check(contract)
summary = {
"schema": contract.schema_name,
"version": contract.version,
"links": [link.id for link in contract.jesd_links],
"clock_bindings": {
binding.requirement_id: {
"dt_label": binding.dt_label,
"output_index": binding.output_index,
}
for binding in bindings.clocks
},
"status": "validated",
}
print(json.dumps(summary, indent=2, sort_keys=True))
if __name__ == "__main__":
main()
The portable electrical result in
examples/jif_contract/ad9680.jif-dt.json is generated by pyadi-jif’s
examples/ad9680_jif_dt_contract.py; the independent illustrative ZC706
placement is examples/jif_contract/ad9680.bindings.json. A different carrier
can reuse the contract with a different bindings document.
Build a bidirectional ADRV9009 handoff
The second example constructs RX and TX links with the typed API, binds their AD9528 and FPGA endpoints, and checks the whole handoff before any pipeline or device-tree operation:
python examples/jif_contract/adrv9009_bidirectional.py
"""Build and bind a bidirectional ADRV9009 contract in memory."""
from __future__ import annotations
import json
from adidt.jif_contract import (
ClockBinding,
ClockRequirement,
JesdBinding,
JesdLink,
JesdParameters,
JifDtBindings,
JifDtContract,
Producer,
)
def build_contract() -> JifDtContract:
"""Return solved RX/TX electrical intent with no physical DT placement."""
return JifDtContract(
schema="adi.jif-dt",
producer=Producer(version="0.1.6"),
jesd_links=(
JesdLink(
id="adrv9009.rx",
direction="adc-to-fpga",
converter="ADRV9009_RX",
fpga="zcu102",
standard="jesd204b",
sample_rate_hz=245_760_000,
lane_rate_hz=9_830_400_000,
parameters=JesdParameters(
F=4, K=32, L=2, M=4, N=16, Np=16, S=1, HD=0
),
fpga_config={"type": "qpll"},
),
JesdLink(
id="adrv9009.tx",
direction="fpga-to-dac",
converter="ADRV9009_TX",
fpga="zcu102",
standard="jesd204b",
sample_rate_hz=245_760_000,
lane_rate_hz=4_915_200_000,
parameters=JesdParameters(
F=2, K=32, L=4, M=4, N=16, Np=16, S=1, HD=0
),
fpga_config={"type": "qpll"},
),
),
clock_requirements=(
ClockRequirement(
id="adrv9009.device-clock",
role="converter-device",
sink="ADRV9009",
source="AD9528",
rate_hz=245_760_000,
divider=4,
),
ClockRequirement(
id="adrv9009.sysref",
role="converter-sysref",
sink="ADRV9009",
source="AD9528",
rate_hz=7_680_000,
divider=128,
),
),
metadata={"example": "bidirectional ADRV9009 on ZCU102"},
)
def build_bindings() -> JifDtBindings:
"""Return the independent pyadi-dt board placement for the solved intent."""
return JifDtBindings(
clocks=(
ClockBinding(
requirement_id="adrv9009.device-clock",
dt_label="ad9528",
output_index=1,
),
ClockBinding(
requirement_id="adrv9009.sysref",
dt_label="ad9528",
output_index=3,
),
),
jesd_links=(
JesdBinding(
link_id="adrv9009.rx",
converter_label="adrv9009-phy",
jesd_label="axi_adrv9009_rx_jesd",
xcvr_label="axi_adrv9009_rx_xcvr",
),
JesdBinding(
link_id="adrv9009.tx",
converter_label="adrv9009-phy",
jesd_label="axi_adrv9009_tx_jesd",
xcvr_label="axi_adrv9009_tx_xcvr",
),
),
)
def main() -> None:
"""Validate both link directions before any pipeline or DT operation."""
contract = build_contract()
bindings = build_bindings()
bindings.check(contract)
print(
json.dumps(
{
"links": {
link.id: {
"direction": link.direction,
"lane_rate_hz": link.lane_rate_hz,
}
for link in contract.jesd_links
},
"status": "validated",
},
indent=2,
sort_keys=True,
)
)
if __name__ == "__main__":
main()
Required validation
Contract checks
The typed reference model performs side-effect-free checks for:
exact schema and supported version;
unknown fields and duplicate semantic IDs;
positive integer rates and dividers;
JESD transport identity,
M * S * Np == 8 * L * F;lane-rate consistency using 8b/10b for JESD204B or 64b/66b for JESD204C;
deterministic JSON serialization and validation on load.
Binding and topology checks
Before generating DTS, pyadi-dt must additionally ensure:
every semantic clock and JESD link is bound exactly once;
bindings contain no unknown IDs;
independent clocks do not occupy the same physical output;
clock channel indices are in range for the selected device;
source device and compatible string agree with the board profile;
converter, JESD, and transceiver labels exist exactly once in the XSA;
direction and clock role agree with the target endpoint;
divider/rate and FPGA PLL selections are supported.
Apply checks
Generation should first target a temporary or in-memory tree. DTS lint and
dtc compilation must pass before an explicit file, SD-card, or live apply.
Failure must not silently substitute a hard-coded configuration.
Proposed producer API
pyadi-jif should add an explicit export API without initially changing the
historical return value of system.solve():
solution = system.solve()
handoff = system.export_config(
format="adi.jif-dt",
solution=solution,
)
handoff.to_json_file("solved.jif-dt.json")
The exporter should build links from converter objects rather than parsing computed dictionary keys. It should zip semantic clock names, rates, and solved dividers without exporting list position as a physical channel. The returned handoff should be a detached snapshot with no solver objects reachable through JSON.
Proposed consumer API
A later pyadi-dt integration can accept the typed contract alongside existing raw configuration support:
XsaPipeline().run(
xsa_path="design.xsa",
cfg=contract,
bindings=bindings,
output_dir="out",
)
The consumer sequence is parse, validate, resolve profile, bind semantic IDs, check XSA topology, check capabilities, convert to internal pipeline config, then render and lint.
The adidtc jif clock command should likewise migrate from positional divider
application to contract plus profile bindings. Legacy JSON can remain behind an
explicit compatibility option for one deprecation cycle, but it must not guess
physical channel placement.
Errors
Use narrow errors at each boundary:
ContractErrorfor malformed or incompatible interchange;BindingErrorfor incomplete, duplicate, or impossible profile placement;TopologyMismatchErrorwhen an expected XSA/DT endpoint is absent or has the wrong direction;CapabilityErrorfor unsupported rates, dividers, or link modes;existing rendering, compilation, and application exceptions afterward.
Do not catch Exception around solve, export, or binding. A caller may choose a
named fallback configuration, but it must be reported as a fallback rather than
as the pyadi-jif solution.
Test agreement
pyadi-jif producer tests
Golden exports for AD9680, nested ADRV9009, and one MxFE JESD204C system.
Exported values match converter objects and historical solve output.
Requested-clock reordering does not alter semantic identity.
Exported snapshots have detached ownership and deterministic JSON.
Gekko and CPLEX produce contract-equivalent fields for the same constraints.
No solver/backend object is JSON reachable.
pyadi-dt consumer tests
The reference suite covers valid parse/bind, unsupported versions, unknown fields, JESD and lane-rate inconsistencies, duplicate IDs, incomplete bindings, physical clock collisions, deterministic round trips, and malformed JSON.
Integration tests should add contract-to-pipeline mapping, profile resolution, XSA mismatch without output mutation, DTS golden output plus compilation/lint, and CLI dry-run before any write.
Cross-repository CI
pyadi-jif should own a small canonical fixture corpus and publish the generated JSON Schema as a release artifact. On pyadi-jif changes, generate fixtures and run the pyadi-dt consumer. On pyadi-dt changes, consume both canonical fixtures and artifacts generated by the minimum supported pyadi-jif release and current main. Hardware-backed configurations can run nightly.
Compatibility policy
Optional additions may be made within contract 1.x without changing existing meaning. Required fields, unit changes, enum semantic changes, and ID-rule changes require contract 2.0. Producer and consumer package versions are provenance; compatibility is determined by the contract version.
Rollout
Agree on names, ownership, and AD9680/ADRV9009/AD9081 canonical systems.
Generate the canonical JSON Schema from the pyadi-jif producer model.
Add
system.export_config(format="adi.jif-dt")and producer tests.Add pyadi-dt profile bindings and contract-to-pipeline translation.
Convert
adidtc jif clockand require dry-run validation.Convert board helpers and remove broad solver fallback.
Enable cross-repository CI, then deprecate positional/raw JSON.
Acceptance requires that saved output works without solver imports, requested clock ordering cannot change physical placement, all endpoints bind exactly once, and invalid input fails before any tree mutation.