dau_core.cores

Core registry models and accessors: every HDL building block, what it is, what it depends on, which golden defines its semantics, and what it costs.

The registry itself lives in dau-core’s hydra config tree (dau_core/config/dau-core/cores.yaml): one CoreDefinition entry per core, composed into the ccflow ModelRegistry at /dau-core/<name> and discoverable on the shared search path via the hydra.lernaplugins entry point. Composition IS the registry — this module defines the models and thin cached accessors, never a core list. Resource measurements are produced by the dau-build synthesize-cores task and recorded per part, clock and module parameters; there is no other shape.

Attributes

HDL_CLOSURE_DIGEST_VERSION

_HDL_INCLUDE

Exceptions

UnknownCoreSourceError

A core names an HDL package that is not installed.

UnrenderedCoreError

A generated core was asked for its source before it was rendered.

UnmeasuredResourceError

Base class for lookup errors.

TimingViolationError

Inappropriate argument value (of correct type).

Classes

CoreKind

Create a collection of name/value pairs.

LoadImageKind

What a load-capable tile's LOAD-phase input stream is (the two-phase

CoreRole

What a core is FOR where a composition has to know, declared by the

ContractKind

Which streaming interface contract an operator tile speaks. Contract

ResourceEnvelope

Resource counts: one LUT/FF/BRAM/DSP tuple. The usable face of a

TimingStatus

Create a collection of name/value pairs.

ResourceMeasurement

One exact OOC result. Coordinates are deliberately discrete: a

IngestRate

A stream core's guaranteed sustained ingest rate: rows rows per

ParameterSpec

An overridable SystemVerilog module parameter: its HDL default and the

StorageIntent

A core's declared storage need (STORAGE_AND_NOC.md). owned = a

CoreDefinition

One HDL building block and its contract surround.

Functions

_same_clock(a, b) → bool

Two clock periods naming the same physical clock.

package_root(package) → pathlib.Path

The on-disk directory of an HDL package.

render_root([package]) → pathlib.Path

Where generated cores are materialized.

Module Contents

class dau_core.cores.CoreKind(*args, **kwds)

Bases: enum.Enum

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

>>> Color.RED
<Color.RED: 1>
  • value lookup:

>>> Color(1)
<Color.RED: 1>
  • name lookup:

>>> Color['RED']
<Color.RED: 1>

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

OPERATOR = 'operator'
STREAM_INFRA = 'stream-infra'
SHELL_ADAPTER = 'shell-adapter'
PACKAGE = 'package'
SIM_MODEL = 'sim-model'
class dau_core.cores.LoadImageKind(*args, **kwds)

Bases: enum.Enum

What a load-capable tile’s LOAD-phase input stream is (the two-phase load: cfg_load=1 streams the build image, cfg_load=0 probes). The composition validates image/length-grid rules per kind; a core without this marker has no cfg_load port and can never be phase-bound.

BITMAP_64 = 'bitmap-64'
QUAD_ROWS = 'quad-rows'
class dau_core.cores.CoreRole(*args, **kwds)

Bases: enum.Enum

What a core is FOR where a composition has to know, declared by the registry rather than recognized by its name.

Two composition rules cannot be read off a tile’s ports: that the core in the optional front-stage feed-transform slot is one that widens packed rows, and that a key-routing partitioner’s lane count satisfies the decode its MODE selects. Both were exact-name comparisons, which is the one thing “composition IS the registry” forbids – a second unpacker or a second router could not be written without editing this package, and a tile rename silently turned the check into dead code. A core says here what job it does; the composition asks.

KEY_DISPATCH is a contract as well as a label: a core declaring it routes whole records to lanes by a decode over the record’s key, and carries MODE (a dau_core.operators.DispatchMode) and NUM_PARTITIONS parameters with those meanings. A range partitioner routes by ordered splitters instead and is deliberately not one.

FRONT_UNPACK = 'front-unpack'
KEY_DISPATCH = 'key-dispatch'
SCAN_READER = 'scan-reader'
SCAN_BROADCAST = 'scan-broadcast'
SCAN_RECORD_WRITER = 'scan-record-writer'
SCAN_WRITE_MUX = 'scan-write-mux'
SHELL_REGISTERS = 'shell-registers'
STREAM_JOB_SHELL = 'stream-job-shell'
FRONT_GEARBOX = 'front-gearbox'
class dau_core.cores.ContractKind(*args, **kwds)

Bases: enum.Enum

Which streaming interface contract an operator tile speaks. Contract tests select their validator from this tag; a new core declares its contract here rather than being special-cased in test code.

ROW_STREAM = 'row-stream'
ARROW_LITE_DESCRIPTOR = 'arrow-lite-descriptor'
WIDE_ROW_STREAM = 'wide-row-stream'
class dau_core.cores.ResourceEnvelope(/, **data: Any)

Bases: dau_core.models.CheckedModel

Resource counts: one LUT/FF/BRAM/DSP tuple. The usable face of a measurement point, or the sum a composition estimate adds up to. Never a registry entry on its own: the registry records measurement points, which carry the coordinates a count is meaningless without.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

lut: pydantic.NonNegativeInt
ff: pydantic.NonNegativeInt
bram36: pydantic.NonNegativeFloat
dsp: pydantic.NonNegativeInt
class dau_core.cores.TimingStatus(*args, **kwds)

Bases: enum.Enum

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

>>> Color.RED
<Color.RED: 1>
  • value lookup:

>>> Color(1)
<Color.RED: 1>
  • name lookup:

>>> Color['RED']
<Color.RED: 1>

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

MET = 'met'
VIOLATED = 'violated'
class dau_core.cores.ResourceMeasurement(/, **data: Any)

Bases: dau_core.models.CheckedModel

One exact OOC result. Coordinates are deliberately discrete: a measurement says nothing about another part, clock, or parameter tuple.

A combinational core has no clock: clock_ns is None, no timing was reported and none is recorded, and the point answers for any clock because the logic costs the same at every one. A clocked point carries the clock it was constrained to, whether it met, and by how much.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

part: str
clock_ns: float | None = None
params: Annotated[collections.abc.Mapping[str, pydantic.StrictInt], *ReadOnly]
lut: pydantic.NonNegativeInt
ff: pydantic.NonNegativeInt
bram36: pydantic.NonNegativeFloat
dsp: pydantic.NonNegativeInt
timing: TimingStatus | None = None
wns_ns: float | None = None
classmethod _accept_type_spellings(data)

A coordinate may name an element type where the synthesis run took one (“FIELD_WIDTH: int32”), and is stored as the encoding, so it compares equal to the same point written as a number. No parameter of an SV module has a legal non-numeric value other than a type, so a string here needs no further disambiguation.

_timing_matches_wns() → ResourceMeasurement
answers_clock(clock_ns: float) → bool

Whether this point prices a build at clock_ns: an unclocked point prices any clock, a clocked one only its own.

usable_envelope() → ResourceEnvelope

Return resource counts only when timing met its target (or there was no timing to meet).

class dau_core.cores.IngestRate(/, **data: Any)

Bases: dau_core.models.CheckedModel

A stream core’s guaranteed sustained ingest rate: rows rows per per_cycles clock cycles, an exact rational (1 row per 2 cycles is the composed lane rate the silicon proved; the key-mask dispatcher ingests 1 per 1). This is PUBLIC INTERFACE, not a heuristic: the scheduler/pricing software rate-matches a core it cannot inspect — a user-provided custom core tied to a custom operator is opaque except for its registry declaration, so the declaration carries the rate. Declare the FLOOR a core sustains under continuous valid input, with RECORD/DRAIN EMISSION AMORTIZED over the batch: close-out records, top-k drains, and per-group records are batch-tail or per-group costs, not per-row costs, so the declaration describes the streaming regime (groups much smaller than rows, drains at close). Data-dependent worst cases — every group key distinct, adversarial key runs into one dispatcher lane — are planner-regime concerns the planner sees in the plan (group keys, sortedness), not floor violations. A core may burst faster, and a measured improvement (a retime, a wider port) lands here the same release the RTL proves it. Compositions over these floors yield the sustained rate the scheduler plans on UNDER those stated assumptions.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

rows: pydantic.StrictInt
per_cycles: pydantic.StrictInt
scales_with_param: str | None = None
frames_on_param: str | None = None
classmethod _positive(value: int) → int
_one_scaling_shape() → IngestRate
property rows_per_cycle: fractions.Fraction
effective(params: collections.abc.Mapping[str, int], declared: collections.abc.Mapping[str, ParameterSpec]) → fractions.Fraction

The rate at concrete parameter values — what the scheduler uses. params are the composition’s overrides; declared the core’s parameter specs (defaults fill unset overrides).

class dau_core.cores.ParameterSpec(/, **data: Any)

Bases: dau_core.models.CheckedModel

An overridable SystemVerilog module parameter: its HDL default and the constraint a composition override must satisfy — mirroring the RTL’s own elaboration guards so an invalid override is rejected at composition time, never later as an HDL elaboration failure. multiple_of mirrors sizing (the membership bitmap is KEY_SPACE/64 words); choices pins an exact legal set (the quad-stream widths are 64 or 128, nothing else); power_of_two mirrors mask/bit-select decodes (the key-mask dispatcher’s NUM_PARTITIONS); minimum mirrors a lower bound (the rolling-moment window is >= 2 — a size-1 sample variance is undefined). The HDL is the source of truth; this mirrors it.

element_type marks the parameter as naming WHAT AN ELEMENT IS rather than how big something is, and changes three things:

  • default and choices may be written as type spellings (int32, q16.16, float64) as well as integers, and are stored as the dau_core.element_types encoding either way. A plain signed integer encodes as its own width, so 32 and int32 are the same declaration and no existing entry changes meaning.

  • choices are matched on the whole type. int32 and q16.16 are both 32 bits wide and are NOT interchangeable, which is the distinction a width could not draw.

  • minimum/maximum/multiple_of/power_of_two constrain the type’s WIDTH, because that is what the RTL guard they mirror is about (“FIELD_WIDTH must divide 128 and lie in [8, 64]” is a statement about the field’s bits, not about the number that names its type).

Set it only for a parameter that genuinely selects an element type. Bus and beat widths (IN_WIDTH, OUT_WIDTH, DATA_WIDTH) count bits per handshake and are not types; declaring one here would let a design ask for a fixed-point AXI bus.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

default: int
multiple_of: int
choices: tuple[int, ...] | None = None
power_of_two: bool = False
minimum: int = 1
maximum: int | None = None
element_type: bool = False
classmethod _accept_type_spellings(data)

Resolve declared type spellings to their encoding before the int fields see them, so everything downstream – the constraint checks, the -generic a synthesis run passes, the canonical spec hash – works on one integer form and never has to know how it was written.

violation(value: int | str) → str | None

Why value is not a legal override, or None when it is. This is the ONE statement of the constraint – composition wraps it with tile context, and the model’s own coherence check runs the default through it, so the rule cannot drift between the two.

normalize(value: int | str) → int

The integer the RTL elaborates with, however the value was written.

A consumer holding a raw override – a build task assembling -generic arguments, say – has to turn a spelling into its code before it reaches a tool, and cannot import this package to do it: dau-build is public and the encoding lives here. Asking the spec that declared the parameter keeps the vocabulary in one place.

_violation(value: int, magnitude: int) → str | None

value is what the RTL elaborates with; magnitude is what the bounds constrain – the same number for a size parameter, and the element type’s width for a type parameter.

_spell(value: int) → str

How a value appears in a message: a type parameter’s values are named, because ‘4128’ tells a reader nothing and ‘q16.16’ does.

_constraints_cohere() → ParameterSpec

The spec mirrors RTL elaboration guards, so an incoherent spec is a registry bug that would reject every override – catch it at registration, not at the first composition.

class dau_core.cores.StorageIntent(/, **data: Any)

Bases: dau_core.models.CheckedModel

A core’s declared storage need (STORAGE_AND_NOC.md). owned = a private single-cycle arena; shared = handle-based tier transactions. Capacity prices against a declared module parameter so the planner can refuse what no tier serves — never discovered at synthesis.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

role: str
access: Literal['owned', 'shared']
bytes_per_unit: int
scale_parameter: str | None = None
tier_hint: str | None = None
dau_core.cores._same_clock(a: float, b: float) → bool

Two clock periods naming the same physical clock.

The unit is NANOSECONDS, so the tolerance is a picosecond: 6.66667 against a derived 6.666666666666667 differs by 3.3e-6 ns. Clock tiers in this registry are more than a nanosecond apart (8.0 vs 6.667 vs 10.0), so a picosecond is a thousand times too small to reach a neighbour.

dau_core.cores.HDL_CLOSURE_DIGEST_VERSION = '1'
dau_core.cores._HDL_INCLUDE
class dau_core.cores.CoreDefinition(/, **data: Any)

Bases: dau_core.models.CheckedModel

One HDL building block and its contract surround.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

name: str
kind: CoreKind
module: str
source: str
source_package: str = 'dau_core.hdl'
generator: dau_core.generators.CoreGenerator | None = None
description: str
depends: tuple[str, ...] = ()
golden: str | None = None
bench: str | None = None
resources: tuple[ResourceMeasurement, ...] | None = None
measured_from: str | None = None
count_port: str | None = None
contract_kind: ContractKind | None = None
role: CoreRole | None = None
accuracy_ulp: int | None = None
ingest: IngestRate | None = None
record_words: pydantic.StrictInt | None = None
record_words_param: str | None = None
output_record_words: pydantic.StrictInt | None = None
output_record_words_param: str | None = None
output_record_words_variable: bool = False
supports_pipeline_depth: bool = False
opcodes: tuple[dau_core.stream.OperationCode, ...] = ()
opcode_input_types: tuple[dau_core.stream.LogicalType, ...] = ()
row_capacity: int | None = None
unit_kind: dau_core.design.UnitKind | None = None
success_close_out: bool = False
load_image: LoadImageKind | None = None
parameters: Annotated[collections.abc.Mapping[str, ParameterSpec], *ReadOnly]
storage_intent: tuple[StorageIntent, ...] = ()
storage_bytes(params: collections.abc.Mapping[str, int]) → tuple[tuple[StorageIntent, int], ...]

Each intent with its byte cost at the effective parameters (overrides win, declared defaults otherwise).

model_post_init(context) → None

Override this method to perform additional initialization after __init__ and model_construct. This is useful if you want to do some validation that requires the entire model to be initialized.

_parameter_value(name: str, params: collections.abc.Mapping[str, int]) → int
effective_record_words(params: collections.abc.Mapping[str, int]) → int

Concrete input-record width after applying parameter overrides.

effective_output_record_words(params: collections.abc.Mapping[str, int]) → int | None

Concrete emitted width, or None for runtime-variable output.

resource_measurement(*, part: str, clock_ns: float, params: collections.abc.Mapping[str, int]) → ResourceMeasurement

Resolve one exact measurement, never a neighboring point.

Composition overrides are expanded with declared defaults before comparison. An unclocked (combinational) point answers for any clock.

Clock periods are DERIVED (1000 / job_clock_mhz), so they are compared to the picosecond rather than by float equality: 150 MHz yields 6.666666666666667 while a registry writes 6.66667, and exact equality rejected a point while printing it in the same breath. Real clock tiers are more than a nanosecond apart, so this cannot reach a neighbour.

source_path() → pathlib.Path

The HDL this core synthesizes from.

A GENERATED core (one declaring a generator) has no checked-in file, so it refuses until it has been rendered rather than rendering implicitly on first read. What sits on disk is what gets synthesized; rendering behind a read would hide which parameters produced it, and the whole reason this core is generated is that its constants are a function of those parameters.

hdl_closure(resolve) → tuple[tuple[str, str], ...]

Every HDL file this core synthesizes from, as (name, sha256) pairs sorted by name.

WHAT IS IN THE CLOSURE is the whole question, and the answer is: the files a synthesis run of THIS core reads. That is its own source, the source of every core reachable through depends (the packages it imports), and every header those files `` include `, resolved the way the toolchain resolves them – beside the including file first, then along the other source directories, which is exactly the -include_dirs list the synthesis task builds. A header nothing includes is NOT in the closure, so adding a second one to the palette does not re-measure every tile that ignores it; and a package a tile imports IS, so an edit there reaches every tile that reads it.

resolve maps a core name to its definition. It is a parameter rather than a registry import because the build package reaches these models through the composed ccflow registry and must not import this one.

A GENERATED core is hashed from its RENDERED TEXT, not from a file on disk: its RTL is a function of the generator and of the constants in its registry entry, so an edit to either changes what synthesis reads while every checked-in file stays byte-identical.

hdl_closure_digest(resolve) → str

The digest a measured envelope records as measured_from.

Over BYTES, deliberately. A digest cannot tell a retimed adder from a renamed module, and pretending otherwise would mean teaching it which edits are allowed to be free – which is the judgement the guard exists to force someone to make and write down. So a rename moves the digest too, and re-stamping it costs an argument in the pull request.

render(*, into: pathlib.Path | None = None) → pathlib.Path

Render this core’s HDL, returning the written path.

golden_callable()
exception dau_core.cores.UnknownCoreSourceError

Bases: RuntimeError

A core names an HDL package that is not installed.

Initialize self. See help(type(self)) for accurate signature.

exception dau_core.cores.UnrenderedCoreError

Bases: RuntimeError

A generated core was asked for its source before it was rendered.

Initialize self. See help(type(self)) for accurate signature.

exception dau_core.cores.UnmeasuredResourceError

Bases: LookupError

Base class for lookup errors.

Initialize self. See help(type(self)) for accurate signature.

exception dau_core.cores.TimingViolationError

Bases: ValueError

Inappropriate argument value (of correct type).

Initialize self. See help(type(self)) for accurate signature.

dau_core.cores.package_root(package: str) → pathlib.Path

The on-disk directory of an HDL package.

Resolved through importlib rather than a relative path from this file, so a core contributed by another distribution finds its own tree.

dau_core.cores.render_root(package: str = 'dau_core.hdl') → pathlib.Path

Where generated cores are materialized.

Defaults beside the declaring package so a checkout behaves like one with the files committed; a build tree points DAU_CORE_RENDER_ROOT at its own directory so two builds cannot race on one file.