dau-core 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. dau_core/__init__.py +118 -0
  2. dau_core/_lazy.py +46 -0
  3. dau_core/capabilities.py +79 -0
  4. dau_core/capability_contract.py +274 -0
  5. dau_core/composition.py +939 -0
  6. dau_core/config/__init__.py +155 -0
  7. dau_core/config/base.yaml +3 -0
  8. dau_core/config/dau-core/cores.yaml +4564 -0
  9. dau_core/config/task/tasks/temporal/hero-workflow.yaml +10 -0
  10. dau_core/cores.py +839 -0
  11. dau_core/design.py +107 -0
  12. dau_core/element_types.py +165 -0
  13. dau_core/exp_generator.py +397 -0
  14. dau_core/fixed_point.py +469 -0
  15. dau_core/generators.py +105 -0
  16. dau_core/hdl/.gitignore +1 -0
  17. dau_core/hdl/__init__.py +123 -0
  18. dau_core/hdl/dau_aggregation_pkg.sv +40 -0
  19. dau_core/hdl/dau_arrow_lite_stream_aggregation.sv +447 -0
  20. dau_core/hdl/dau_asof_backward.sv +412 -0
  21. dau_core/hdl/dau_axi_burst_reader.sv +219 -0
  22. dau_core/hdl/dau_axi_ram_sim.sv +209 -0
  23. dau_core/hdl/dau_axi_random_probe.sv +180 -0
  24. dau_core/hdl/dau_axi_record_writer.sv +233 -0
  25. dau_core/hdl/dau_axi_stream_job.sv +379 -0
  26. dau_core/hdl/dau_axi_write_mux.sv +125 -0
  27. dau_core/hdl/dau_bar_aggregation.sv +388 -0
  28. dau_core/hdl/dau_bitonic_sorter.sv +278 -0
  29. dau_core/hdl/dau_bram_stream_job.sv +175 -0
  30. dau_core/hdl/dau_broadcast_hash_join.sv +522 -0
  31. dau_core/hdl/dau_datetime_format.sv +489 -0
  32. dau_core/hdl/dau_field_sum_aggregation.sv +262 -0
  33. dau_core/hdl/dau_fixed_point_divide.sv +305 -0
  34. dau_core/hdl/dau_fixed_point_sqrt.sv +285 -0
  35. dau_core/hdl/dau_grouped_aggregation.sv +370 -0
  36. dau_core/hdl/dau_grouped_field_aggregation.sv +333 -0
  37. dau_core/hdl/dau_grouped_field_sum_aggregation.sv +321 -0
  38. dau_core/hdl/dau_hash_grouped_aggregation.sv +519 -0
  39. dau_core/hdl/dau_identity_axil.v +135 -0
  40. dau_core/hdl/dau_identity_registers.sv +66 -0
  41. dau_core/hdl/dau_int32_keyed_map_alu.sv +227 -0
  42. dau_core/hdl/dau_int32_keyed_predicate_filter.sv +250 -0
  43. dau_core/hdl/dau_int32_map_alu.sv +228 -0
  44. dau_core/hdl/dau_int32_predicate_filter.sv +297 -0
  45. dau_core/hdl/dau_int32_record_gather.sv +244 -0
  46. dau_core/hdl/dau_key_mask_dispatcher.sv +592 -0
  47. dau_core/hdl/dau_key_membership_filter.sv +526 -0
  48. dau_core/hdl/dau_keyed_state.sv +316 -0
  49. dau_core/hdl/dau_map_alu_pkg.sv +74 -0
  50. dau_core/hdl/dau_market_feature.sv +262 -0
  51. dau_core/hdl/dau_pair_key_filter.sv +251 -0
  52. dau_core/hdl/dau_predicate_pkg.sv +70 -0
  53. dau_core/hdl/dau_range_partitioner.sv +341 -0
  54. dau_core/hdl/dau_record_batch_aggregation.sv +492 -0
  55. dau_core/hdl/dau_record_gearbox.sv +188 -0
  56. dau_core/hdl/dau_rolling_extremum.sv +389 -0
  57. dau_core/hdl/dau_rolling_moments.sv +484 -0
  58. dau_core/hdl/dau_row_map_alu.sv +312 -0
  59. dau_core/hdl/dau_row_predicate_filter.sv +416 -0
  60. dau_core/hdl/dau_row_unpack.sv +578 -0
  61. dau_core/hdl/dau_scratchpad.sv +230 -0
  62. dau_core/hdl/dau_session_gap.sv +439 -0
  63. dau_core/hdl/dau_stream_aggregation.sv +178 -0
  64. dau_core/hdl/dau_stream_broadcast.sv +41 -0
  65. dau_core/hdl/dau_streaming_dedup.sv +418 -0
  66. dau_core/hdl/dau_streaming_top_k.sv +275 -0
  67. dau_core/hdl/dau_stride_distributor.sv +250 -0
  68. dau_core/hdl/dau_time_bucket_key.sv +269 -0
  69. dau_core/hdl/dau_wide_field_sum_aggregation.sv +346 -0
  70. dau_core/hdl/dau_wide_row_map_alu.sv +304 -0
  71. dau_core/hdl/dau_wide_row_predicate_filter.sv +364 -0
  72. dau_core/hdl/types.svh +71 -0
  73. dau_core/hero.py +204 -0
  74. dau_core/ln_generator.py +447 -0
  75. dau_core/models.py +126 -0
  76. dau_core/normal_cdf_generator.py +318 -0
  77. dau_core/operators.py +1993 -0
  78. dau_core/registers.py +464 -0
  79. dau_core/registry.py +194 -0
  80. dau_core/stream.py +356 -0
  81. dau_core/temporal.py +522 -0
  82. dau_core/tests/cocotb_benches/__init__.py +1 -0
  83. dau_core/tests/cocotb_benches/dau_arrow_lite_stream_aggregation_tb.py +189 -0
  84. dau_core/tests/cocotb_benches/dau_asof_backward_tb.py +288 -0
  85. dau_core/tests/cocotb_benches/dau_asof_feature_chain_tb.py +252 -0
  86. dau_core/tests/cocotb_benches/dau_axi_bar_job_tb.py +176 -0
  87. dau_core/tests/cocotb_benches/dau_axi_bar_noc_tb.py +207 -0
  88. dau_core/tests/cocotb_benches/dau_axi_burst_reader_tb.py +103 -0
  89. dau_core/tests/cocotb_benches/dau_axi_random_probe_tb.py +104 -0
  90. dau_core/tests/cocotb_benches/dau_axi_sort_noc_tb.py +241 -0
  91. dau_core/tests/cocotb_benches/dau_bar_aggregation_tb.py +220 -0
  92. dau_core/tests/cocotb_benches/dau_bitonic_sorter_tb.py +344 -0
  93. dau_core/tests/cocotb_benches/dau_broadcast_hash_join_small_tb.py +71 -0
  94. dau_core/tests/cocotb_benches/dau_broadcast_hash_join_tb.py +760 -0
  95. dau_core/tests/cocotb_benches/dau_bucket_agg_chain_tb.py +163 -0
  96. dau_core/tests/cocotb_benches/dau_datetime_format_tb.py +236 -0
  97. dau_core/tests/cocotb_benches/dau_field_sum_aggregation_tb.py +371 -0
  98. dau_core/tests/cocotb_benches/dau_fixed_point_divide_tb.py +393 -0
  99. dau_core/tests/cocotb_benches/dau_fixed_point_exp_tb.py +130 -0
  100. dau_core/tests/cocotb_benches/dau_fixed_point_ln_tb.py +248 -0
  101. dau_core/tests/cocotb_benches/dau_fixed_point_sqrt_tb.py +367 -0
  102. dau_core/tests/cocotb_benches/dau_grouped_aggregation_tb.py +206 -0
  103. dau_core/tests/cocotb_benches/dau_grouped_field_aggregation_tb.py +200 -0
  104. dau_core/tests/cocotb_benches/dau_grouped_field_sum_aggregation_tb.py +472 -0
  105. dau_core/tests/cocotb_benches/dau_hash_grouped_aggregation_tb.py +328 -0
  106. dau_core/tests/cocotb_benches/dau_hero_datapath_tb.py +179 -0
  107. dau_core/tests/cocotb_benches/dau_int32_keyed_map_alu_tb.py +410 -0
  108. dau_core/tests/cocotb_benches/dau_int32_keyed_predicate_filter_tb.py +418 -0
  109. dau_core/tests/cocotb_benches/dau_int32_map_alu_tb.py +206 -0
  110. dau_core/tests/cocotb_benches/dau_int32_predicate_filter_tb.py +193 -0
  111. dau_core/tests/cocotb_benches/dau_int32_record_gather_tb.py +256 -0
  112. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_exact_tb.py +130 -0
  113. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_hash_tb.py +127 -0
  114. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_multi_tb.py +158 -0
  115. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_records_tb.py +172 -0
  116. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_tb.py +269 -0
  117. dau_core/tests/cocotb_benches/dau_key_mask_dispatcher_wide_tb.py +193 -0
  118. dau_core/tests/cocotb_benches/dau_key_membership_filter_single_word_tb.py +97 -0
  119. dau_core/tests/cocotb_benches/dau_key_membership_filter_tb.py +625 -0
  120. dau_core/tests/cocotb_benches/dau_keyed_state_tb.py +196 -0
  121. dau_core/tests/cocotb_benches/dau_market_feature_tb.py +196 -0
  122. dau_core/tests/cocotb_benches/dau_normal_cdf_tb.py +145 -0
  123. dau_core/tests/cocotb_benches/dau_pair_key_filter_tb.py +296 -0
  124. dau_core/tests/cocotb_benches/dau_range_partitioner_tb.py +402 -0
  125. dau_core/tests/cocotb_benches/dau_record_batch_aggregation_tb.py +369 -0
  126. dau_core/tests/cocotb_benches/dau_record_gearbox_tb.py +229 -0
  127. dau_core/tests/cocotb_benches/dau_rolling_extremum_tb.py +252 -0
  128. dau_core/tests/cocotb_benches/dau_rolling_moments_tb.py +207 -0
  129. dau_core/tests/cocotb_benches/dau_row_map_alu_tb.py +339 -0
  130. dau_core/tests/cocotb_benches/dau_row_predicate_filter_tb.py +587 -0
  131. dau_core/tests/cocotb_benches/dau_row_predicate_filter_width_tb.py +150 -0
  132. dau_core/tests/cocotb_benches/dau_row_unpack_slots_tb.py +349 -0
  133. dau_core/tests/cocotb_benches/dau_row_unpack_tb.py +783 -0
  134. dau_core/tests/cocotb_benches/dau_row_unpack_wide_tb.py +428 -0
  135. dau_core/tests/cocotb_benches/dau_scratchpad_tb.py +194 -0
  136. dau_core/tests/cocotb_benches/dau_session_gap_tb.py +208 -0
  137. dau_core/tests/cocotb_benches/dau_stream_aggregation_tb.py +94 -0
  138. dau_core/tests/cocotb_benches/dau_streaming_dedup_tb.py +312 -0
  139. dau_core/tests/cocotb_benches/dau_streaming_top_k_tb.py +209 -0
  140. dau_core/tests/cocotb_benches/dau_stride_distributor_tb.py +120 -0
  141. dau_core/tests/cocotb_benches/dau_time_bucket_key_tb.py +182 -0
  142. dau_core/tests/cocotb_benches/dau_wide_field_sum_aggregation_tb.py +169 -0
  143. dau_core/tests/cocotb_benches/dau_wide_ingest_chain_tb.py +141 -0
  144. dau_core/tests/cocotb_benches/dau_wide_row_map_alu_tb.py +255 -0
  145. dau_core/tests/cocotb_benches/dau_wide_row_predicate_filter_tb.py +284 -0
  146. dau_core/tests/cocotb_benches/rate_probe.py +95 -0
  147. dau_core/tests/conftest.py +22 -0
  148. dau_core/tests/exhaustive_sweep.py +90 -0
  149. dau_core/tests/fixtures/ohlcv/README.md +17 -0
  150. dau_core/tests/fixtures/ohlcv/bars_full.csv +8 -0
  151. dau_core/tests/fixtures/ohlcv/bars_regular.csv +7 -0
  152. dau_core/tests/fixtures/ohlcv/prepared_full.csv +13 -0
  153. dau_core/tests/fixtures/ohlcv/prepared_regular.csv +12 -0
  154. dau_core/tests/fixtures/ohlcv/trades.csv +14 -0
  155. dau_core/tests/sv/dau_arrow_lite_stream_aggregation_tb.sv +250 -0
  156. dau_core/tests/sv/dau_asof_feature_chain.sv +149 -0
  157. dau_core/tests/sv/dau_axi_bar_job_sim.sv +195 -0
  158. dau_core/tests/sv/dau_axi_bar_job_tb.sv +130 -0
  159. dau_core/tests/sv/dau_axi_random_probe_sim.sv +73 -0
  160. dau_core/tests/sv/dau_bar_aggregation_tb.sv +151 -0
  161. dau_core/tests/sv/dau_bucket_agg_chain.sv +78 -0
  162. dau_core/tests/sv/dau_grouped_aggregation_tb.sv +170 -0
  163. dau_core/tests/sv/dau_hero_datapath.sv +230 -0
  164. dau_core/tests/sv/dau_int32_map_alu_tb.sv +170 -0
  165. dau_core/tests/sv/dau_int32_predicate_filter_tb.sv +165 -0
  166. dau_core/tests/sv/dau_record_batch_aggregation_tb.sv +214 -0
  167. dau_core/tests/sv/dau_stream_aggregation_tb.sv +170 -0
  168. dau_core/tests/sv/dau_wide_ingest_chain.sv +73 -0
  169. dau_core/tests/test_aggregation_goldens.py +43 -0
  170. dau_core/tests/test_aggregation_tile_hdl.py +71 -0
  171. dau_core/tests/test_benches_can_fail.py +125 -0
  172. dau_core/tests/test_capability_contract.py +235 -0
  173. dau_core/tests/test_composition.py +1342 -0
  174. dau_core/tests/test_contributed_cores.py +112 -0
  175. dau_core/tests/test_contributed_discovery.py +156 -0
  176. dau_core/tests/test_core_contract.py +38 -0
  177. dau_core/tests/test_cores.py +1006 -0
  178. dau_core/tests/test_datetime_format.py +125 -0
  179. dau_core/tests/test_design.py +58 -0
  180. dau_core/tests/test_docs.py +49 -0
  181. dau_core/tests/test_element_types.py +204 -0
  182. dau_core/tests/test_fixed_point_exp.py +145 -0
  183. dau_core/tests/test_fixed_point_ln.py +140 -0
  184. dau_core/tests/test_fixed_point_normal_cdf.py +72 -0
  185. dau_core/tests/test_generated_cores.py +131 -0
  186. dau_core/tests/test_hash_grouped.py +157 -0
  187. dau_core/tests/test_hdl_cocotb.py +1092 -0
  188. dau_core/tests/test_hdl_elaboration.py +167 -0
  189. dau_core/tests/test_hdl_parameter_bindings_resolve.py +87 -0
  190. dau_core/tests/test_hdl_sources.py +53 -0
  191. dau_core/tests/test_hdl_verilator.py +156 -0
  192. dau_core/tests/test_hero.py +163 -0
  193. dau_core/tests/test_lazy_exports.py +78 -0
  194. dau_core/tests/test_measured_from_matches_the_hdl.py +234 -0
  195. dau_core/tests/test_ohlcv_fixtures.py +82 -0
  196. dau_core/tests/test_operators.py +1359 -0
  197. dau_core/tests/test_output_beat_declarations.py +91 -0
  198. dau_core/tests/test_parameter_bounds_match_the_hdl.py +144 -0
  199. dau_core/tests/test_public_tile_surface.py +175 -0
  200. dau_core/tests/test_readme.py +26 -0
  201. dau_core/tests/test_record_batch.py +113 -0
  202. dau_core/tests/test_registers_and_capabilities.py +320 -0
  203. dau_core/tests/test_stream.py +73 -0
  204. dau_core/tests/test_temporal.py +242 -0
  205. dau_core/tests/test_validation_at_use.py +222 -0
  206. dau_core/workflows.py +53 -0
  207. dau_core-0.1.1.dist-info/METADATA +292 -0
  208. dau_core-0.1.1.dist-info/RECORD +211 -0
  209. dau_core-0.1.1.dist-info/WHEEL +4 -0
  210. dau_core-0.1.1.dist-info/entry_points.txt +2 -0
  211. dau_core-0.1.1.dist-info/licenses/LICENSE +201 -0
dau_core/__init__.py ADDED
@@ -0,0 +1,118 @@
1
+ """dau-core: the tile palette, the device contracts and the goldens.
2
+
3
+ Exports load on first use (:mod:`dau_core._lazy`). Importing this package
4
+ must not import ccflow and its stack, because hydra's searchpath plugin
5
+ imports ``dau_core.config`` to discover what is installed, and discovery
6
+ wants none of these names.
7
+ """
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from ._lazy import lazy_exports
12
+
13
+ if TYPE_CHECKING:
14
+ from .capabilities import DeviceCapabilities, DmaConstraints, OperatorCapability
15
+ from .capability_contract import (
16
+ CAPABILITY_CONTRACT_SCHEMA,
17
+ CapabilityContract,
18
+ LaneCapability,
19
+ TileCapability,
20
+ capability_contract,
21
+ opcode_bitmap,
22
+ tile_capability,
23
+ )
24
+ from .design import DauDesign, OperatorUnit, UnitKind, format_units, parse_units
25
+ from .operators import (
26
+ EmptyInputError,
27
+ UnsupportedAggregationError,
28
+ aggregate_values,
29
+ pack_splitters,
30
+ synthetic_values,
31
+ )
32
+ from .registers import (
33
+ CURRENT_REGISTER_MAP_VERSION,
34
+ DAU_MAGIC_WORD,
35
+ DEFAULT_STREAM_JOB_REGISTER_CONTRACT,
36
+ JobControlFlag,
37
+ JobStatusFlag,
38
+ RegisterBlockSnapshot,
39
+ RegisterMapVersion,
40
+ RegisterOffset,
41
+ RegisterWriteError,
42
+ ResetScope,
43
+ StreamJobRegisterContract,
44
+ StreamProtocolVersion,
45
+ )
46
+ from .stream import (
47
+ DEFAULT_RECORD_BATCH_LIMITS,
48
+ BatchHeader,
49
+ ColumnDescriptor,
50
+ GroupRole,
51
+ InvalidStreamError,
52
+ LogicalType,
53
+ OperationCode,
54
+ OperationDescriptor,
55
+ PolicyFlag,
56
+ RecordBatchLimits,
57
+ StreamAggregationErrorCode,
58
+ StreamFlag,
59
+ validate_record_batch,
60
+ )
61
+
62
+ __version__ = "0.1.1"
63
+
64
+ # {submodule: the names it exports}. ``__all__`` and the lazy resolver both
65
+ # come from this one declaration, so a name cannot be in one and missing
66
+ # from the other; ``tests/test_lazy_exports.py`` checks it against the
67
+ # TYPE_CHECKING block above and resolves every name.
68
+ _EXPORTS = {
69
+ "capabilities": ("DeviceCapabilities", "DmaConstraints", "OperatorCapability"),
70
+ "capability_contract": (
71
+ "CAPABILITY_CONTRACT_SCHEMA",
72
+ "CapabilityContract",
73
+ "LaneCapability",
74
+ "TileCapability",
75
+ "capability_contract",
76
+ "opcode_bitmap",
77
+ "tile_capability",
78
+ ),
79
+ "design": ("DauDesign", "OperatorUnit", "UnitKind", "format_units", "parse_units"),
80
+ "operators": (
81
+ "EmptyInputError",
82
+ "UnsupportedAggregationError",
83
+ "aggregate_values",
84
+ "pack_splitters",
85
+ "synthetic_values",
86
+ ),
87
+ "registers": (
88
+ "CURRENT_REGISTER_MAP_VERSION",
89
+ "DAU_MAGIC_WORD",
90
+ "DEFAULT_STREAM_JOB_REGISTER_CONTRACT",
91
+ "JobControlFlag",
92
+ "JobStatusFlag",
93
+ "RegisterBlockSnapshot",
94
+ "RegisterMapVersion",
95
+ "RegisterOffset",
96
+ "RegisterWriteError",
97
+ "ResetScope",
98
+ "StreamJobRegisterContract",
99
+ "StreamProtocolVersion",
100
+ ),
101
+ "stream": (
102
+ "DEFAULT_RECORD_BATCH_LIMITS",
103
+ "BatchHeader",
104
+ "ColumnDescriptor",
105
+ "GroupRole",
106
+ "InvalidStreamError",
107
+ "LogicalType",
108
+ "OperationCode",
109
+ "OperationDescriptor",
110
+ "PolicyFlag",
111
+ "RecordBatchLimits",
112
+ "StreamAggregationErrorCode",
113
+ "StreamFlag",
114
+ "validate_record_batch",
115
+ ),
116
+ }
117
+
118
+ __getattr__, __all__ = lazy_exports(__name__, _EXPORTS)
dau_core/_lazy.py ADDED
@@ -0,0 +1,46 @@
1
+ """Import a package's exports on first use.
2
+
3
+ Every model in this stack is a ccflow model, and importing ccflow imports
4
+ csp, pandas and the rest of its stack: about 0.9 s. A package whose
5
+ ``__init__`` imports its model modules therefore pays all of it just to be
6
+ imported -- including when hydra's searchpath plugin imports the config
7
+ package to discover what is installed, which is the slow step in composing
8
+ the registry and wants none of those names.
9
+
10
+ So a package declares which submodule provides each exported name and this
11
+ builds the ``__getattr__`` that imports the one submodule the name needs.
12
+ The names stay importable exactly as before (``from dau_core import
13
+ RegisterOffset``), and a type checker reads them from the ``TYPE_CHECKING``
14
+ block the package keeps beside the declaration.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from collections.abc import Callable
20
+ from importlib import import_module
21
+
22
+ __all__ = ("lazy_exports",)
23
+
24
+
25
+ def lazy_exports(package: str, exports: dict[str, tuple[str, ...]]) -> tuple[Callable[[str], object], list[str]]:
26
+ """A module ``__getattr__`` and ``__all__`` for ``{submodule: names}``.
27
+
28
+ Returns both so the two cannot disagree: a name exported by nothing is
29
+ not reachable, and a name declared here and missing from its submodule
30
+ raises on first access rather than at import, which a guard test is what
31
+ turns back into an import-time error.
32
+ """
33
+ providers = {}
34
+ for module, names in exports.items():
35
+ for name in names:
36
+ if name in providers:
37
+ raise ValueError(f"{package}: {name!r} is exported by both {providers[name]!r} and {module!r}")
38
+ providers[name] = module
39
+
40
+ def getattr_(name: str) -> object:
41
+ module = providers.get(name)
42
+ if module is None:
43
+ raise AttributeError(f"module {package!r} has no attribute {name!r}")
44
+ return getattr(import_module(f"{package}.{module}"), name)
45
+
46
+ return getattr_, sorted(providers)
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .registers import RegisterMapVersion, StreamProtocolVersion
6
+ from .stream import LogicalType, OperationCode
7
+
8
+ # DAU platform variant 1 (dpv1) identity — the single home for these
9
+ # values; drivers, orchestration, and tooling import them rather than
10
+ # repeating literals. The wire identity word is the platform-variant name
11
+ # "DPV1" (the board's own product name is an implementation detail); shells
12
+ # report it in the PLATFORM_ID register.
13
+ DPV1_PLATFORM_ID = "DPV1"
14
+ XILINX_VENDOR_ID = 0x10EE
15
+ DPV1_DEVICE_ID = 0x7011
16
+ DPV1_PCI_ID = f"{XILINX_VENDOR_ID:04x}:{DPV1_DEVICE_ID:04x}"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DmaConstraints:
21
+ alignment_bytes: int
22
+ descriptor_bytes: int
23
+ max_transfer_bytes: int
24
+ read_channels: int
25
+ write_channels: int
26
+
27
+ def __post_init__(self) -> None:
28
+ if self.alignment_bytes <= 0:
29
+ raise ValueError("alignment_bytes must be positive")
30
+ if self.descriptor_bytes <= 0:
31
+ raise ValueError("descriptor_bytes must be positive")
32
+ if self.max_transfer_bytes <= 0:
33
+ raise ValueError("max_transfer_bytes must be positive")
34
+ if self.read_channels < 0 or self.write_channels < 0:
35
+ raise ValueError("DMA channel counts cannot be negative")
36
+
37
+ def is_aligned(self, value: int) -> bool:
38
+ return value % self.alignment_bytes == 0
39
+
40
+ @property
41
+ def max_aligned_transfer_bytes(self) -> int:
42
+ return self.max_transfer_bytes - (self.max_transfer_bytes % self.alignment_bytes)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class OperatorCapability:
47
+ opcode: OperationCode
48
+ input_types: tuple[LogicalType, ...]
49
+ output_type: LogicalType
50
+ supports_group_by: bool
51
+
52
+ def supports(self, opcode: OperationCode, logical_type: LogicalType) -> bool:
53
+ return self.opcode == opcode and logical_type in self.input_types
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class DeviceCapabilities:
58
+ """What a probed device advertises.
59
+
60
+ ``operators`` are hardware capabilities — opcodes some tile executes in
61
+ HDL. ``host_operators`` are protocol-level capabilities the host derives
62
+ from hardware results (AVERAGE = host divide of SUM/COUNT); they must
63
+ never be submitted to the device as operations.
64
+ """
65
+
66
+ platform_id: str
67
+ pci_vendor_id: int
68
+ pci_device_id: int
69
+ register_map_version: RegisterMapVersion
70
+ stream_protocol_version: StreamProtocolVersion
71
+ dma: DmaConstraints
72
+ operators: tuple[OperatorCapability, ...]
73
+ host_operators: tuple[OperatorCapability, ...] = ()
74
+
75
+ def supports(self, opcode: OperationCode, logical_type: LogicalType) -> bool:
76
+ return any(operator.supports(opcode, logical_type) for operator in self.operators)
77
+
78
+ def supports_host_computed(self, opcode: OperationCode, logical_type: LogicalType) -> bool:
79
+ return any(operator.supports(opcode, logical_type) for operator in self.host_operators)
@@ -0,0 +1,274 @@
1
+ """Composition-derived capability contract: the single source of truth for
2
+ what a generated shell can do.
3
+
4
+ ``capability_contract(spec)`` derives the contract from the same
5
+ ``ScanCompositionSpec`` that generates the shell top, using the registry's
6
+ declared HDL facts (opcodes, input types, count ports, contract kinds, row
7
+ capacities). Three consumers read it:
8
+
9
+ - the identity register block (``operator_bitmap`` / ``lane_count`` /
10
+ ``host_opcode_bitmap`` / ``sort_capacity`` words, register map 0.2);
11
+ - the shell-build artlink manifest (``to_mapping()``, JSON-able);
12
+ - host planners (``to_design()`` for the pushdown scheduler,
13
+ driver capability decode via the manifest mapping).
14
+
15
+ Hardware truth vs protocol truth is explicit: ``operator_bitmap`` carries
16
+ only OperationCodes some tile executes in HDL (MIN/MAX/SUM/COUNT);
17
+ AVERAGE is never a hardware opcode — when a tile provides both SUM and
18
+ COUNT it appears in ``host_opcodes`` (the host divides the results at
19
+ protocol level), never in ``opcodes``.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Mapping
25
+ from typing import Any
26
+
27
+ from pydantic import Field
28
+
29
+ from .composition import ScanCompositionSpec, TileSpec
30
+ from .cores import ContractKind
31
+ from .design import DauDesign, OperatorUnit, UnitKind
32
+ from .models import CheckedModel
33
+ from .registers import CURRENT_REGISTER_MAP_VERSION, RegisterMapVersion
34
+ from .registry import core
35
+ from .stream import LogicalType, OperationCode
36
+
37
+ CAPABILITY_CONTRACT_SCHEMA = 1
38
+
39
+
40
+ def opcode_bitmap(opcodes: tuple[OperationCode, ...]) -> int:
41
+ """The register encoding: bit ``1 << opcode`` per OperationCode."""
42
+ bitmap = 0
43
+ for opcode in opcodes:
44
+ bitmap |= 1 << int(opcode)
45
+ return bitmap
46
+
47
+
48
+ class TileCapability(CheckedModel):
49
+ """One operator tile instance as the contract describes it: registry
50
+ identity, stream contract, HDL opcode truth, and instance config."""
51
+
52
+ core: str
53
+ module: str
54
+ contract_kind: ContractKind | None
55
+ count_port: str | None
56
+ opcodes: tuple[OperationCode, ...] = ()
57
+ input_types: tuple[LogicalType, ...] = ()
58
+ unit_kind: UnitKind | None = None
59
+ row_capacity: int | None = None
60
+ config: tuple[tuple[str, str], ...] = ()
61
+
62
+ @property
63
+ def host_opcodes(self) -> tuple[OperationCode, ...]:
64
+ """Protocol-level opcodes the host can compute from this tile's
65
+ hardware results: AVERAGE = SUM/COUNT divided host-side. Never
66
+ hardware opcodes and never advertised as such."""
67
+ if OperationCode.SUM in self.opcodes and OperationCode.COUNT in self.opcodes:
68
+ return (OperationCode.AVERAGE,)
69
+ return ()
70
+
71
+ def to_mapping(self) -> dict[str, Any]:
72
+ return {
73
+ "core": self.core,
74
+ "module": self.module,
75
+ "contract_kind": self.contract_kind.value if self.contract_kind is not None else None,
76
+ "count_port": self.count_port,
77
+ "opcodes": [opcode.name.lower() for opcode in self.opcodes],
78
+ "input_types": [logical_type.name.lower() for logical_type in self.input_types],
79
+ "unit_kind": self.unit_kind.value if self.unit_kind is not None else None,
80
+ "row_capacity": self.row_capacity,
81
+ "config": dict(self.config),
82
+ }
83
+
84
+ @classmethod
85
+ def from_mapping(cls, mapping: Mapping[str, Any]) -> TileCapability:
86
+ return cls(
87
+ core=mapping["core"],
88
+ module=mapping["module"],
89
+ contract_kind=ContractKind(mapping["contract_kind"]) if mapping.get("contract_kind") else None,
90
+ count_port=mapping.get("count_port"),
91
+ opcodes=tuple(OperationCode[token.upper()] for token in mapping.get("opcodes", ())),
92
+ input_types=tuple(LogicalType[token.upper()] for token in mapping.get("input_types", ())),
93
+ unit_kind=UnitKind(mapping["unit_kind"]) if mapping.get("unit_kind") else None,
94
+ row_capacity=mapping.get("row_capacity"),
95
+ config=tuple(sorted((str(key), str(value)) for key, value in dict(mapping.get("config", {})).items())),
96
+ )
97
+
98
+
99
+ class LaneCapability(CheckedModel):
100
+ """One scan lane: the terminal operator tile, its optional partition
101
+ filter, and the ordered mid-lane operator ``chain`` between them."""
102
+
103
+ tile: TileCapability
104
+ partition: TileCapability | None = None
105
+ chain: tuple[TileCapability, ...] = ()
106
+
107
+ def to_mapping(self) -> dict[str, Any]:
108
+ return {
109
+ "tile": self.tile.to_mapping(),
110
+ "partition": self.partition.to_mapping() if self.partition is not None else None,
111
+ "chain": [stage.to_mapping() for stage in self.chain],
112
+ }
113
+
114
+ @classmethod
115
+ def from_mapping(cls, mapping: Mapping[str, Any]) -> LaneCapability:
116
+ partition = mapping.get("partition")
117
+ return cls(
118
+ tile=TileCapability.from_mapping(mapping["tile"]),
119
+ partition=TileCapability.from_mapping(partition) if partition else None,
120
+ chain=tuple(TileCapability.from_mapping(stage) for stage in mapping.get("chain", ())),
121
+ )
122
+
123
+
124
+ class CapabilityContract(CheckedModel):
125
+ """The advertised capability truth of one composed design.
126
+
127
+ A config-data model (it is what the shell-build manifest carries and
128
+ what a planner matches against), so a ccflow model like the rest of the
129
+ registry's declarations; ``to_mapping``/``from_mapping`` remain the
130
+ manifest serialization, unchanged, because manifests already on disk
131
+ carry that form."""
132
+
133
+ name: str
134
+ lanes: tuple[LaneCapability, ...]
135
+ partitioner: TileCapability | None = None
136
+ register_map_version: RegisterMapVersion = Field(default_factory=lambda: CURRENT_REGISTER_MAP_VERSION)
137
+ # whether the lanes' chain config ports are runtime registers the host
138
+ # rewrites per job, so a planner matches this design structurally
139
+ # (enough stages of the right kinds) instead of config for config. A
140
+ # fact about the bitstream, carried in its manifest; never a caller's
141
+ # declaration.
142
+ reconfigurable: bool = False
143
+
144
+ @property
145
+ def lane_count(self) -> int:
146
+ return len(self.lanes)
147
+
148
+ @property
149
+ def hardware_opcodes(self) -> tuple[OperationCode, ...]:
150
+ """Union of the lanes' HDL opcodes (terminal tiles and chain
151
+ stages), ordered by opcode value."""
152
+ opcodes = {opcode for lane in self.lanes for tile in (lane.tile, *lane.chain) for opcode in tile.opcodes}
153
+ return tuple(sorted(opcodes, key=int))
154
+
155
+ @property
156
+ def host_opcodes(self) -> tuple[OperationCode, ...]:
157
+ """Protocol-level opcodes derivable host-side (AVERAGE when a tile
158
+ provides both SUM and COUNT); explicitly not hardware opcodes."""
159
+ opcodes = {opcode for lane in self.lanes for tile in (lane.tile, *lane.chain) for opcode in tile.host_opcodes}
160
+ return tuple(sorted(opcodes, key=int))
161
+
162
+ @property
163
+ def operator_bitmap(self) -> int:
164
+ return opcode_bitmap(self.hardware_opcodes)
165
+
166
+ @property
167
+ def host_opcode_bitmap(self) -> int:
168
+ return opcode_bitmap(self.host_opcodes)
169
+
170
+ @property
171
+ def sort_capacity(self) -> int:
172
+ capacities = [lane.tile.row_capacity or 0 for lane in self.lanes if lane.tile.unit_kind == UnitKind.SORT]
173
+ return max(capacities, default=0)
174
+
175
+ def to_design(self) -> DauDesign:
176
+ """The operator inventory this contract advertises: identical lane
177
+ tiles that contribute a design unit collapse into one counted unit."""
178
+ counts: dict[TileCapability, int] = {}
179
+ for lane in self.lanes:
180
+ if lane.tile.unit_kind is None:
181
+ continue
182
+ key = TileCapability(
183
+ core=lane.tile.core,
184
+ module=lane.tile.module,
185
+ contract_kind=lane.tile.contract_kind,
186
+ count_port=lane.tile.count_port,
187
+ opcodes=lane.tile.opcodes,
188
+ input_types=lane.tile.input_types,
189
+ unit_kind=lane.tile.unit_kind,
190
+ row_capacity=lane.tile.row_capacity,
191
+ )
192
+ counts[key] = counts.get(key, 0) + 1
193
+ unit_list: list[OperatorUnit] = []
194
+ for tile, count in counts.items():
195
+ if tile.unit_kind is not None:
196
+ unit_list.append(OperatorUnit(kind=tile.unit_kind, count=count, opcodes=tile.opcodes, input_types=tile.input_types))
197
+ units = tuple(unit_list)
198
+ return DauDesign(name=self.name, units=units)
199
+
200
+ def to_mapping(self) -> dict[str, Any]:
201
+ """JSON-able serialization for the shell-build manifest."""
202
+ return {
203
+ "contract": "dau-capability-contract",
204
+ "schema": CAPABILITY_CONTRACT_SCHEMA,
205
+ "name": self.name,
206
+ "register_map_version": f"{self.register_map_version.major}.{self.register_map_version.minor}",
207
+ "operator_bitmap": f"0x{self.operator_bitmap:08x}",
208
+ "host_opcode_bitmap": f"0x{self.host_opcode_bitmap:08x}",
209
+ "lane_count": self.lane_count,
210
+ "sort_capacity": self.sort_capacity,
211
+ "lanes": [lane.to_mapping() for lane in self.lanes],
212
+ "partitioner": self.partitioner.to_mapping() if self.partitioner is not None else None,
213
+ "reconfigurable": self.reconfigurable,
214
+ }
215
+
216
+ @classmethod
217
+ def from_mapping(cls, mapping: Mapping[str, Any]) -> CapabilityContract:
218
+ if mapping.get("contract") != "dau-capability-contract":
219
+ raise ValueError("not a dau capability contract mapping")
220
+ schema = mapping.get("schema")
221
+ if schema != CAPABILITY_CONTRACT_SCHEMA:
222
+ raise ValueError(f"unsupported capability contract schema {schema!r}")
223
+ major, _, minor = str(mapping["register_map_version"]).partition(".")
224
+ partitioner = mapping.get("partitioner")
225
+ return cls(
226
+ name=mapping["name"],
227
+ lanes=tuple(LaneCapability.from_mapping(lane) for lane in mapping.get("lanes", ())),
228
+ partitioner=TileCapability.from_mapping(partitioner) if partitioner else None,
229
+ register_map_version=RegisterMapVersion(int(major), int(minor)),
230
+ # absent in manifests written before the fact was advertised:
231
+ # every one of those designs binds its configs as literals
232
+ reconfigurable=bool(mapping.get("reconfigurable", False)),
233
+ )
234
+
235
+
236
+ def tile_capability(tile: TileSpec | str) -> TileCapability:
237
+ """The registry-declared capability facts of one tile instance."""
238
+ spec = TileSpec(core=tile) if isinstance(tile, str) else tile
239
+ definition = core(spec.core)
240
+ return TileCapability(
241
+ core=definition.name,
242
+ module=definition.module,
243
+ contract_kind=definition.contract_kind,
244
+ count_port=definition.count_port,
245
+ opcodes=definition.opcodes,
246
+ input_types=definition.opcode_input_types,
247
+ unit_kind=definition.unit_kind,
248
+ row_capacity=definition.row_capacity,
249
+ config=tuple(sorted((str(key), str(value)) for key, value in spec.config.items())),
250
+ )
251
+
252
+
253
+ def capability_contract(spec: ScanCompositionSpec) -> CapabilityContract:
254
+ """Derive the capability contract from the composition that generates
255
+ the shell top — same spec, same registry, one source of truth.
256
+
257
+ ``reconfigurable`` stays false: the generated walker binds every config
258
+ port to the literal the composition names, so no composed design has
259
+ runtime config registers yet. When the generator can bind them to
260
+ registers, the composition will say so and this is where the contract
261
+ reads it."""
262
+ spec = spec.check()
263
+ return CapabilityContract(
264
+ name=spec.name,
265
+ lanes=tuple(
266
+ LaneCapability(
267
+ tile=tile_capability(lane.tile),
268
+ partition=tile_capability(lane.partition) if lane.partition is not None else None,
269
+ chain=tuple(tile_capability(stage) for stage in lane.chain),
270
+ )
271
+ for lane in spec.lanes
272
+ ),
273
+ partitioner=tile_capability(spec.partitioner) if spec.partitioner is not None else None,
274
+ )