oscura 0.5.1__py3-none-any.whl → 0.7.0__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 (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/reporting/plots.py CHANGED
@@ -101,93 +101,114 @@ class PlotGenerator:
101
101
  ... output_manager
102
102
  ... )
103
103
  """
104
- from oscura.reporting.config import AnalysisDomain
105
104
 
106
- # Get plot format and DPI from config
105
+ plot_format, plot_dpi = self._get_plot_settings()
106
+ saved_paths: list[Path] = []
107
+
108
+ saved_paths.extend(
109
+ self._generate_registered_plots(results, domain, output_manager, plot_format, plot_dpi)
110
+ )
111
+
112
+ saved_paths.extend(
113
+ self._generate_domain_plots(domain, results, output_manager, plot_format, plot_dpi)
114
+ )
115
+
116
+ return saved_paths
117
+
118
+ def _get_plot_settings(self) -> tuple[str, int]:
119
+ """Get plot format and DPI from config."""
107
120
  plot_format = self.config.plot_format if self.config else "png"
108
121
  plot_dpi = self.config.plot_dpi if self.config else 150
122
+ return plot_format, plot_dpi
109
123
 
124
+ def _generate_registered_plots(
125
+ self,
126
+ results: dict[str, Any],
127
+ domain: AnalysisDomain,
128
+ output_manager: OutputManager,
129
+ plot_format: str,
130
+ plot_dpi: int,
131
+ ) -> list[Path]:
132
+ """Generate plots using registered plot functions."""
110
133
  saved_paths: list[Path] = []
111
134
 
112
135
  for analysis_name, result_data in results.items():
113
- # Skip non-dict results
114
136
  if not isinstance(result_data, dict):
115
137
  continue
116
138
 
117
- # Check if we have a registered plot function
118
139
  key = (domain, analysis_name)
119
140
  if key in PLOT_REGISTRY:
120
- plot_func = PLOT_REGISTRY[key]
121
- try:
122
- fig = plot_func(result_data)
123
- if fig is not None:
124
- path = output_manager.save_plot(
125
- domain,
126
- analysis_name,
127
- fig,
128
- format=plot_format,
129
- dpi=plot_dpi,
130
- )
131
- saved_paths.append(path)
132
- plt.close(fig) # Prevent memory leaks
133
- except Exception as e:
134
- # Log error but continue with other plots
135
- logger.warning("Failed to generate %s plot: %s", analysis_name, e)
136
- continue
137
-
138
- # Also try generic domain-level plots
139
- try:
140
- if domain == AnalysisDomain.SPECTRAL:
141
- saved_paths.extend(
142
- self._generate_spectral_plots(
143
- results, domain, output_manager, plot_format, plot_dpi
144
- )
145
- )
146
- elif domain == AnalysisDomain.WAVEFORM:
147
- saved_paths.extend(
148
- self._generate_waveform_plots(
149
- results, domain, output_manager, plot_format, plot_dpi
150
- )
151
- )
152
- elif domain == AnalysisDomain.DIGITAL:
153
- saved_paths.extend(
154
- self._generate_digital_plots(
155
- results, domain, output_manager, plot_format, plot_dpi
156
- )
157
- )
158
- elif domain == AnalysisDomain.STATISTICS:
159
- saved_paths.extend(
160
- self._generate_statistics_plots(
161
- results, domain, output_manager, plot_format, plot_dpi
162
- )
163
- )
164
- elif domain == AnalysisDomain.JITTER:
165
- saved_paths.extend(
166
- self._generate_jitter_plots(
167
- results, domain, output_manager, plot_format, plot_dpi
168
- )
169
- )
170
- elif domain == AnalysisDomain.EYE:
171
- saved_paths.extend(
172
- self._generate_eye_plots(results, domain, output_manager, plot_format, plot_dpi)
173
- )
174
- elif domain == AnalysisDomain.PATTERNS:
175
- saved_paths.extend(
176
- self._generate_pattern_plots(
177
- results, domain, output_manager, plot_format, plot_dpi
178
- )
179
- )
180
- elif domain == AnalysisDomain.POWER:
181
- saved_paths.extend(
182
- self._generate_power_plots(
183
- results, domain, output_manager, plot_format, plot_dpi
184
- )
141
+ path = self._try_generate_plot(
142
+ PLOT_REGISTRY[key],
143
+ result_data,
144
+ domain,
145
+ analysis_name,
146
+ output_manager,
147
+ plot_format,
148
+ plot_dpi,
185
149
  )
186
- except Exception as e:
187
- logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
150
+ if path is not None:
151
+ saved_paths.append(path)
188
152
 
189
153
  return saved_paths
190
154
 
155
+ def _try_generate_plot(
156
+ self,
157
+ plot_func: Callable[[dict[str, Any]], Figure | None],
158
+ result_data: dict[str, Any],
159
+ domain: AnalysisDomain,
160
+ analysis_name: str,
161
+ output_manager: OutputManager,
162
+ plot_format: str,
163
+ plot_dpi: int,
164
+ ) -> Path | None:
165
+ """Attempt to generate and save a single plot."""
166
+ try:
167
+ fig = plot_func(result_data)
168
+ if fig is None:
169
+ return None
170
+
171
+ path = output_manager.save_plot(
172
+ domain, analysis_name, fig, format=plot_format, dpi=plot_dpi
173
+ )
174
+ plt.close(fig)
175
+ return path
176
+ except Exception as e:
177
+ logger.warning("Failed to generate %s plot: %s", analysis_name, e)
178
+ return None
179
+
180
+ def _generate_domain_plots(
181
+ self,
182
+ domain: AnalysisDomain,
183
+ results: dict[str, Any],
184
+ output_manager: OutputManager,
185
+ plot_format: str,
186
+ plot_dpi: int,
187
+ ) -> list[Path]:
188
+ """Generate domain-specific plots based on analysis domain."""
189
+ from oscura.reporting.config import AnalysisDomain
190
+
191
+ domain_generators = {
192
+ AnalysisDomain.SPECTRAL: self._generate_spectral_plots,
193
+ AnalysisDomain.WAVEFORM: self._generate_waveform_plots,
194
+ AnalysisDomain.DIGITAL: self._generate_digital_plots,
195
+ AnalysisDomain.STATISTICS: self._generate_statistics_plots,
196
+ AnalysisDomain.JITTER: self._generate_jitter_plots,
197
+ AnalysisDomain.EYE: self._generate_eye_plots,
198
+ AnalysisDomain.PATTERNS: self._generate_pattern_plots,
199
+ AnalysisDomain.POWER: self._generate_power_plots,
200
+ }
201
+
202
+ generator = domain_generators.get(domain)
203
+ if generator is None:
204
+ return []
205
+
206
+ try:
207
+ return generator(results, domain, output_manager, plot_format, plot_dpi)
208
+ except Exception as e:
209
+ logger.warning("Error in domain-level plot generation for %s: %s", domain.value, e)
210
+ return []
211
+
191
212
  def _generate_spectral_plots(
192
213
  self,
193
214
  results: dict[str, Any],
@@ -199,52 +220,110 @@ class PlotGenerator:
199
220
  """Generate spectral analysis plots (FFT, PSD, spectrogram)."""
200
221
  paths: list[Path] = []
201
222
 
202
- # FFT plot
203
- if "fft" in results and isinstance(results["fft"], dict):
204
- fft_data = results["fft"]
205
- if "frequencies" in fft_data and "magnitude_db" in fft_data:
206
- try:
207
- fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
208
- path = output_manager.save_plot(
209
- domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
210
- )
211
- paths.append(path)
212
- plt.close(fig)
213
- except Exception:
214
- pass
223
+ # Generate FFT plot
224
+ fft_path = self._try_generate_fft_plot(
225
+ results, domain, output_manager, plot_format, plot_dpi
226
+ )
227
+ if fft_path:
228
+ paths.append(fft_path)
215
229
 
216
- # PSD plot
217
- if "psd" in results and isinstance(results["psd"], dict):
218
- psd_data = results["psd"]
219
- if "frequencies" in psd_data and "psd" in psd_data:
220
- try:
221
- fig = self._plot_spectrum(
222
- psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
223
- )
224
- path = output_manager.save_plot(
225
- domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
226
- )
227
- paths.append(path)
228
- plt.close(fig)
229
- except Exception:
230
- pass
230
+ # Generate PSD plot
231
+ psd_path = self._try_generate_psd_plot(
232
+ results, domain, output_manager, plot_format, plot_dpi
233
+ )
234
+ if psd_path:
235
+ paths.append(psd_path)
231
236
 
232
- # Spectrogram
233
- if "spectrogram" in results and isinstance(results["spectrogram"], dict):
234
- spec_data = results["spectrogram"]
235
- if "times" in spec_data and "frequencies" in spec_data and "Sxx_db" in spec_data:
236
- try:
237
- fig = self._plot_spectrogram(spec_data)
238
- path = output_manager.save_plot(
239
- domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
240
- )
241
- paths.append(path)
242
- plt.close(fig)
243
- except Exception:
244
- pass
237
+ # Generate spectrogram
238
+ spec_path = self._try_generate_spectrogram(
239
+ results, domain, output_manager, plot_format, plot_dpi
240
+ )
241
+ if spec_path:
242
+ paths.append(spec_path)
245
243
 
246
244
  return paths
247
245
 
246
+ def _try_generate_fft_plot(
247
+ self,
248
+ results: dict[str, Any],
249
+ domain: AnalysisDomain,
250
+ output_manager: OutputManager,
251
+ plot_format: str,
252
+ plot_dpi: int,
253
+ ) -> Path | None:
254
+ """Try to generate FFT spectrum plot."""
255
+ if "fft" not in results or not isinstance(results["fft"], dict):
256
+ return None
257
+
258
+ fft_data = results["fft"]
259
+ if "frequencies" not in fft_data or "magnitude_db" not in fft_data:
260
+ return None
261
+
262
+ try:
263
+ fig = self._plot_spectrum(fft_data, title="FFT Magnitude Spectrum")
264
+ path = output_manager.save_plot(
265
+ domain, "fft_spectrum", fig, format=plot_format, dpi=plot_dpi
266
+ )
267
+ plt.close(fig)
268
+ return path
269
+ except Exception:
270
+ return None
271
+
272
+ def _try_generate_psd_plot(
273
+ self,
274
+ results: dict[str, Any],
275
+ domain: AnalysisDomain,
276
+ output_manager: OutputManager,
277
+ plot_format: str,
278
+ plot_dpi: int,
279
+ ) -> Path | None:
280
+ """Try to generate PSD plot."""
281
+ if "psd" not in results or not isinstance(results["psd"], dict):
282
+ return None
283
+
284
+ psd_data = results["psd"]
285
+ if "frequencies" not in psd_data or "psd" not in psd_data:
286
+ return None
287
+
288
+ try:
289
+ fig = self._plot_spectrum(
290
+ psd_data, title="Power Spectral Density", ylabel="PSD (dB/Hz)"
291
+ )
292
+ path = output_manager.save_plot(
293
+ domain, "psd_spectrum", fig, format=plot_format, dpi=plot_dpi
294
+ )
295
+ plt.close(fig)
296
+ return path
297
+ except Exception:
298
+ return None
299
+
300
+ def _try_generate_spectrogram(
301
+ self,
302
+ results: dict[str, Any],
303
+ domain: AnalysisDomain,
304
+ output_manager: OutputManager,
305
+ plot_format: str,
306
+ plot_dpi: int,
307
+ ) -> Path | None:
308
+ """Try to generate spectrogram plot."""
309
+ if "spectrogram" not in results or not isinstance(results["spectrogram"], dict):
310
+ return None
311
+
312
+ spec_data = results["spectrogram"]
313
+ required_keys = ["times", "frequencies", "Sxx_db"]
314
+ if not all(key in spec_data for key in required_keys):
315
+ return None
316
+
317
+ try:
318
+ fig = self._plot_spectrogram(spec_data)
319
+ path = output_manager.save_plot(
320
+ domain, "spectrogram", fig, format=plot_format, dpi=plot_dpi
321
+ )
322
+ plt.close(fig)
323
+ return path
324
+ except Exception:
325
+ return None
326
+
248
327
  def _generate_waveform_plots(
249
328
  self,
250
329
  results: dict[str, Any],
@@ -77,62 +77,103 @@ def create_executive_summary_section(
77
77
  References:
78
78
  REPORT-004, REPORT-006
79
79
  """
80
- content_parts = []
80
+ content_parts: list[str] = []
81
81
 
82
- # Overall status
83
- if "pass_count" in results and "total_count" in results:
84
- pass_count = results["pass_count"]
85
- total = results["total_count"]
86
-
87
- if pass_count == total:
88
- content_parts.append(f"All {total} tests passed with satisfactory margins.")
89
- else:
90
- fail_count = total - pass_count
91
- content_parts.append(
92
- f"{fail_count} of {total} tests failed ({fail_count / total * 100:.0f}% failure rate)."
93
- )
94
-
95
- # Key findings
96
- if key_findings:
97
- content_parts.append("\n**Key Findings:**")
98
- for finding in key_findings[:5]: # Top 5
99
- content_parts.append(f"- {finding}")
100
-
101
- # Margin analysis
102
- if "min_margin" in results:
103
- margin = results["min_margin"]
104
- content_parts.append("\n**Margin Analysis:**")
105
- if margin < 0:
106
- content_parts.append(f"Critical: Minimum margin is {margin:.1f}% (violation).")
107
- elif margin < 10:
108
- content_parts.append(
109
- f"Warning: Minimum margin is {margin:.1f}% (below recommended 10%)."
110
- )
111
- elif margin < 20:
112
- content_parts.append(f"Acceptable: Minimum margin is {margin:.1f}% (below target 20%).")
113
- else:
114
- content_parts.append(f"Good: Minimum margin is {margin:.1f}% (exceeds target 20%).")
115
-
116
- # Recommendations (for detailed summary)
117
- if length == "detailed" and "violations" in results:
118
- violations = results["violations"]
119
- if violations:
120
- content_parts.append("\n**Recommendations:**")
121
- for violation in violations[:3]:
122
- content_parts.append(
123
- f"- Address {violation.get('parameter', 'measurement')} violation"
124
- )
125
-
126
- content = "\n".join(content_parts)
82
+ _add_test_status(content_parts, results)
83
+ _add_key_findings(content_parts, key_findings)
84
+ _add_margin_analysis(content_parts, results)
85
+ _add_recommendations(content_parts, results, length)
127
86
 
128
87
  return Section(
129
88
  title="Executive Summary",
130
- content=content,
89
+ content="\n".join(content_parts),
131
90
  level=1,
132
91
  visible=True,
133
92
  )
134
93
 
135
94
 
95
+ def _add_test_status(content_parts: list[str], results: dict[str, Any]) -> None:
96
+ """Add test status to summary.
97
+
98
+ Args:
99
+ content_parts: List to append to.
100
+ results: Results dictionary.
101
+ """
102
+ if "pass_count" not in results or "total_count" not in results:
103
+ return
104
+
105
+ pass_count = results["pass_count"]
106
+ total = results["total_count"]
107
+
108
+ if pass_count == total:
109
+ content_parts.append(f"All {total} tests passed with satisfactory margins.")
110
+ else:
111
+ fail_count = total - pass_count
112
+ fail_rate = fail_count / total * 100
113
+ content_parts.append(
114
+ f"{fail_count} of {total} tests failed ({fail_rate:.0f}% failure rate)."
115
+ )
116
+
117
+
118
+ def _add_key_findings(content_parts: list[str], key_findings: list[str] | None) -> None:
119
+ """Add key findings to summary.
120
+
121
+ Args:
122
+ content_parts: List to append to.
123
+ key_findings: Findings to add.
124
+ """
125
+ if not key_findings:
126
+ return
127
+
128
+ content_parts.append("\n**Key Findings:**")
129
+ for finding in key_findings[:5]:
130
+ content_parts.append(f"- {finding}")
131
+
132
+
133
+ def _add_margin_analysis(content_parts: list[str], results: dict[str, Any]) -> None:
134
+ """Add margin analysis to summary.
135
+
136
+ Args:
137
+ content_parts: List to append to.
138
+ results: Results dictionary.
139
+ """
140
+ if "min_margin" not in results:
141
+ return
142
+
143
+ margin = results["min_margin"]
144
+ content_parts.append("\n**Margin Analysis:**")
145
+
146
+ if margin < 0:
147
+ content_parts.append(f"Critical: Minimum margin is {margin:.1f}% (violation).")
148
+ elif margin < 10:
149
+ content_parts.append(f"Warning: Minimum margin is {margin:.1f}% (below recommended 10%).")
150
+ elif margin < 20:
151
+ content_parts.append(f"Acceptable: Minimum margin is {margin:.1f}% (below target 20%).")
152
+ else:
153
+ content_parts.append(f"Good: Minimum margin is {margin:.1f}% (exceeds target 20%).")
154
+
155
+
156
+ def _add_recommendations(content_parts: list[str], results: dict[str, Any], length: str) -> None:
157
+ """Add recommendations to summary if detailed.
158
+
159
+ Args:
160
+ content_parts: List to append to.
161
+ results: Results dictionary.
162
+ length: Summary length mode.
163
+ """
164
+ if length != "detailed" or "violations" not in results:
165
+ return
166
+
167
+ violations = results["violations"]
168
+ if not violations:
169
+ return
170
+
171
+ content_parts.append("\n**Recommendations:**")
172
+ for violation in violations[:3]:
173
+ param = violation.get("parameter", "measurement")
174
+ content_parts.append(f"- Address {param} violation")
175
+
176
+
136
177
  def create_measurement_results_section(
137
178
  measurements: dict[str, Any],
138
179
  *,