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
@@ -4,7 +4,7 @@ This module provides compliance report generation in multiple formats.
4
4
 
5
5
 
6
6
  Example:
7
- >>> from oscura.compliance import test_compliance, generate_compliance_report
7
+ >>> from oscura.validation.compliance import test_compliance, generate_compliance_report
8
8
  >>> result = test_compliance(trace, mask)
9
9
  >>> generate_compliance_report(result, 'report.html')
10
10
 
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  from typing import TYPE_CHECKING
21
21
 
22
22
  if TYPE_CHECKING:
23
- from oscura.compliance.testing import ComplianceResult
23
+ from oscura.validation.compliance.testing import ComplianceResult
24
24
 
25
25
 
26
26
  class ComplianceReportFormat(Enum):
@@ -110,37 +110,43 @@ def generate_compliance_report(
110
110
  return output_path
111
111
 
112
112
 
113
- def _generate_html_report(
114
- result: ComplianceResult,
115
- output_path: Path,
116
- *,
117
- include_plot: bool = True,
118
- title: str | None = None,
119
- company_name: str | None = None,
120
- dut_info: dict[str, str] | None = None,
121
- ) -> None:
122
- """Generate HTML compliance report."""
123
- title = title or "EMC Compliance Report"
124
- status_color = "#28a745" if result.passed else "#dc3545"
125
- status_text = "PASS" if result.passed else "FAIL"
113
+ def _generate_dut_section_html(dut_info: dict[str, str] | None) -> str:
114
+ """Generate DUT information section HTML.
126
115
 
127
- # Build DUT info section
128
- dut_section = ""
129
- if dut_info:
130
- dut_rows = "".join(f"<tr><td>{k}</td><td>{v}</td></tr>" for k, v in dut_info.items())
131
- dut_section = f"""
116
+ Args:
117
+ dut_info: Device Under Test information dict.
118
+
119
+ Returns:
120
+ HTML string for DUT section.
121
+ """
122
+ if not dut_info:
123
+ return ""
124
+
125
+ dut_rows = "".join(f"<tr><td>{k}</td><td>{v}</td></tr>" for k, v in dut_info.items())
126
+ return f"""
132
127
  <h3>Device Under Test</h3>
133
128
  <table class="info-table">
134
129
  {dut_rows}
135
130
  </table>
136
131
  """
137
- # Build violations table
138
- violations_section = ""
139
- if result.violations:
140
- violation_rows = ""
141
- for v in result.violations:
142
- freq_mhz = v.frequency / 1e6
143
- violation_rows += f"""
132
+
133
+
134
+ def _generate_violations_section_html(result: ComplianceResult) -> str:
135
+ """Generate violations table section HTML.
136
+
137
+ Args:
138
+ result: ComplianceResult with violations list.
139
+
140
+ Returns:
141
+ HTML string for violations section.
142
+ """
143
+ if not result.violations:
144
+ return ""
145
+
146
+ violation_rows = ""
147
+ for v in result.violations:
148
+ freq_mhz = v.frequency / 1e6
149
+ violation_rows += f"""
144
150
  <tr>
145
151
  <td>{freq_mhz:.3f}</td>
146
152
  <td>{v.measured_level:.1f}</td>
@@ -148,7 +154,8 @@ def _generate_html_report(
148
154
  <td style="color: red;">{v.excess_db:.1f}</td>
149
155
  </tr>
150
156
  """
151
- violations_section = f"""
157
+
158
+ return f"""
152
159
  <h3>Violations ({len(result.violations)})</h3>
153
160
  <table class="data-table">
154
161
  <thead>
@@ -164,22 +171,18 @@ def _generate_html_report(
164
171
  </tbody>
165
172
  </table>
166
173
  """
167
- # Build plot section
168
- plot_section = ""
169
- if include_plot and len(result.spectrum_freq) > 0:
170
- plot_section = _generate_plot_html(result)
171
174
 
172
- # Company header
173
- company_header = ""
174
- if company_name:
175
- company_header = f"<div class='company-name'>{company_name}</div>"
176
175
 
177
- html = f"""<!DOCTYPE html>
178
- <html>
179
- <head>
180
- <meta charset="UTF-8">
181
- <title>{title}</title>
182
- <style>
176
+ def _generate_report_css(status_color: str) -> str:
177
+ """Generate CSS stylesheet for compliance report.
178
+
179
+ Args:
180
+ status_color: Badge color for pass/fail status.
181
+
182
+ Returns:
183
+ CSS stylesheet string.
184
+ """
185
+ return f"""
183
186
  body {{
184
187
  font-family: 'Segoe UI', Arial, sans-serif;
185
188
  max-width: 1200px;
@@ -258,14 +261,19 @@ def _generate_html_report(
258
261
  color: #666;
259
262
  font-size: 12px;
260
263
  }}
261
- </style>
262
- </head>
263
- <body>
264
- {company_header}
265
- <h1>{title}</h1>
264
+ """
266
265
 
267
- <div class="status-badge">{status_text}</div>
268
266
 
267
+ def _generate_summary_grid_html(result: ComplianceResult) -> str:
268
+ """Generate summary grid HTML.
269
+
270
+ Args:
271
+ result: ComplianceResult with summary metrics.
272
+
273
+ Returns:
274
+ HTML string for summary grid.
275
+ """
276
+ return f"""
269
277
  <h2>Test Summary</h2>
270
278
  <div class="summary-grid">
271
279
  <div class="summary-card">
@@ -285,18 +293,84 @@ def _generate_html_report(
285
293
  <div class="value">{len(result.violations)}</div>
286
294
  </div>
287
295
  </div>
296
+ """
288
297
 
289
- {dut_section}
290
298
 
291
- {violations_section}
299
+ def _generate_footer_html(result: ComplianceResult) -> str:
300
+ """Generate footer HTML.
292
301
 
293
- {plot_section}
302
+ Args:
303
+ result: ComplianceResult with metadata.
294
304
 
305
+ Returns:
306
+ HTML string for footer.
307
+ """
308
+ return f"""
295
309
  <div class="footer">
296
310
  <p>Report generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
297
311
  <p>Detector: {result.detector} | Distance: {result.metadata.get("distance", "N/A")}m</p>
298
312
  <p>Generated by Oscura EMC Compliance Module</p>
299
313
  </div>
314
+ """
315
+
316
+
317
+ def _generate_html_report(
318
+ result: ComplianceResult,
319
+ output_path: Path,
320
+ *,
321
+ include_plot: bool = True,
322
+ title: str | None = None,
323
+ company_name: str | None = None,
324
+ dut_info: dict[str, str] | None = None,
325
+ ) -> None:
326
+ """Generate HTML compliance report.
327
+
328
+ Args:
329
+ result: ComplianceResult from test_compliance().
330
+ output_path: Output file path.
331
+ include_plot: Include spectrum/limit plot in report.
332
+ title: Report title (default: "EMC Compliance Report").
333
+ company_name: Company name for header.
334
+ dut_info: Device Under Test information dict.
335
+ """
336
+ title = title or "EMC Compliance Report"
337
+ status_color = "#28a745" if result.passed else "#dc3545"
338
+ status_text = "PASS" if result.passed else "FAIL"
339
+
340
+ # Build sections
341
+ dut_section = _generate_dut_section_html(dut_info)
342
+ violations_section = _generate_violations_section_html(result)
343
+
344
+ plot_section = ""
345
+ if include_plot and len(result.spectrum_freq) > 0:
346
+ plot_section = _generate_plot_html(result)
347
+
348
+ company_header = f"<div class='company-name'>{company_name}</div>" if company_name else ""
349
+
350
+ html = f"""<!DOCTYPE html>
351
+ <html>
352
+ <head>
353
+ <meta charset="UTF-8">
354
+ <title>{title}</title>
355
+ <style>
356
+ {_generate_report_css(status_color)}
357
+ </style>
358
+ </head>
359
+ <body>
360
+ {company_header}
361
+ <h1>{title}</h1>
362
+
363
+ <div class="status-badge">{status_text}</div>
364
+
365
+ {_generate_summary_grid_html(result)}
366
+
367
+ {dut_section}
368
+
369
+ {violations_section}
370
+
371
+ {plot_section}
372
+
373
+ {_generate_footer_html(result)}
300
374
  </body>
301
375
  </html>
302
376
  """
@@ -326,7 +400,7 @@ def _generate_plot_html(result: ComplianceResult) -> str:
326
400
  width - 2 * padding
327
401
  )
328
402
 
329
- def y_scale(l: float) -> float: # noqa: E741
403
+ def y_scale(l: float) -> float:
330
404
  return height - padding - (l - level_min) / (level_max - level_min) * (height - 2 * padding) # type: ignore[no-any-return]
331
405
 
332
406
  # Build spectrum path (downsample for SVG)
@@ -466,7 +540,7 @@ def _convert_html_to_pdf(html_path: Path, pdf_path: Path) -> None:
466
540
  """Convert HTML to PDF using available tools."""
467
541
  try:
468
542
  # Try weasyprint first
469
- from weasyprint import HTML # type: ignore[import-not-found]
543
+ from weasyprint import HTML
470
544
 
471
545
  HTML(str(html_path)).write_pdf(str(pdf_path))
472
546
  except ImportError:
@@ -4,7 +4,7 @@ This module provides compliance testing against regulatory limit masks.
4
4
 
5
5
 
6
6
  Example:
7
- >>> from oscura.compliance import load_limit_mask, test_compliance
7
+ >>> from oscura.validation.compliance import load_limit_mask, test_compliance
8
8
  >>> mask = load_limit_mask('FCC_Part15_ClassB')
9
9
  >>> result = test_compliance(trace, mask)
10
10
  >>> print(f"Status: {result.status}")
@@ -25,8 +25,8 @@ import numpy as np
25
25
  if TYPE_CHECKING:
26
26
  from numpy.typing import NDArray
27
27
 
28
- from oscura.compliance.masks import LimitMask
29
28
  from oscura.core.types import WaveformTrace
29
+ from oscura.validation.compliance.masks import LimitMask
30
30
 
31
31
 
32
32
  class DetectorType(Enum):
@@ -127,39 +127,23 @@ class ComplianceResult:
127
127
  return "\n".join(lines)
128
128
 
129
129
 
130
- def check_compliance(
130
+ def _prepare_spectrum(
131
131
  trace_or_spectrum: WaveformTrace | tuple[NDArray[np.float64], NDArray[np.float64]],
132
- mask: LimitMask,
133
- *,
134
- detector: DetectorType | str = DetectorType.PEAK,
135
- frequency_range: tuple[float, float] | None = None,
136
- unit_conversion: str | None = None,
137
- ) -> ComplianceResult:
138
- """Check signal against EMC limit mask.
132
+ detector: DetectorType,
133
+ unit_conversion: str | None,
134
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
135
+ """Prepare spectrum data from trace or pre-computed spectrum.
139
136
 
140
137
  Args:
141
- trace_or_spectrum: Either a WaveformTrace to analyze, or a tuple of
142
- (frequency_array, magnitude_array) if spectrum already computed.
143
- mask: LimitMask to test against.
144
- detector: Detector type to use ('peak', 'quasi-peak', 'average', 'rms').
145
- frequency_range: Optional (min, max) frequency range to test.
146
- unit_conversion: Optional unit conversion ('V_to_dBuV', 'W_to_dBm', etc.)
138
+ trace_or_spectrum: Either WaveformTrace or (freq, mag) tuple.
139
+ detector: Detector type.
140
+ unit_conversion: Optional unit conversion.
147
141
 
148
142
  Returns:
149
- ComplianceResult with pass/fail status and violation details.
150
-
151
- Example:
152
- >>> mask = load_limit_mask('FCC_Part15_ClassB')
153
- >>> result = check_compliance(trace, mask)
154
- >>> print(result.summary())
143
+ Tuple of (frequency, spectrum_level_dB).
155
144
  """
156
145
  from oscura.core.types import WaveformTrace
157
146
 
158
- # Handle detector type
159
- if isinstance(detector, str):
160
- detector = DetectorType(detector.lower().replace("-", "_").replace(" ", "_"))
161
-
162
- # Get spectrum
163
147
  if isinstance(trace_or_spectrum, WaveformTrace):
164
148
  freq, mag = _compute_spectrum(trace_or_spectrum, detector)
165
149
  else:
@@ -167,19 +151,35 @@ def check_compliance(
167
151
 
168
152
  # Convert to dB if needed
169
153
  if unit_conversion == "V_to_dBuV":
170
- # dBuV = 20*log10(V * 1e6)
171
154
  spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
172
155
  elif unit_conversion == "W_to_dBm":
173
- # dBm = 10*log10(W * 1000)
174
156
  spectrum_level = 10 * np.log10(np.abs(mag) * 1000 + 1e-12)
175
157
  elif mag.max() > 0 and mag.max() < 10:
176
- # Assume linear voltage, convert to dBuV
177
158
  spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
178
159
  else:
179
- # Assume already in dB
180
160
  spectrum_level = mag
181
161
 
182
- # Apply frequency range filter
162
+ return freq, spectrum_level
163
+
164
+
165
+ def _apply_frequency_filters(
166
+ freq: NDArray[np.float64],
167
+ spectrum_level: NDArray[np.float64],
168
+ mask: LimitMask,
169
+ frequency_range: tuple[float, float] | None,
170
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
171
+ """Apply frequency range filters to spectrum.
172
+
173
+ Args:
174
+ freq: Frequency array.
175
+ spectrum_level: Spectrum level array.
176
+ mask: Limit mask.
177
+ frequency_range: Optional user-specified frequency range.
178
+
179
+ Returns:
180
+ Filtered (freq, spectrum_level) tuple.
181
+ """
182
+ # Apply user frequency range filter
183
183
  if frequency_range is not None:
184
184
  f_min, f_max = frequency_range
185
185
  mask_filter = (freq >= f_min) & (freq <= f_max)
@@ -192,30 +192,31 @@ def check_compliance(
192
192
  freq = freq[in_range]
193
193
  spectrum_level = spectrum_level[in_range]
194
194
 
195
- if len(freq) == 0:
196
- # No data in mask range
197
- return ComplianceResult(
198
- status="PASS",
199
- mask_name=mask.name,
200
- violations=[],
201
- margin_to_limit=np.inf,
202
- worst_frequency=0.0,
203
- worst_margin=np.inf,
204
- spectrum_freq=np.array([]),
205
- spectrum_level=np.array([]),
206
- limit_level=np.array([]),
207
- detector=detector.value,
208
- )
195
+ return freq, spectrum_level
209
196
 
210
- # Interpolate limit to spectrum frequencies
211
- limit_level = mask.interpolate(freq)
212
197
 
213
- # Calculate margin (positive = passing)
214
- margin = limit_level - spectrum_level
198
+ def _find_violations(
199
+ freq: NDArray[np.float64],
200
+ spectrum_level: NDArray[np.float64],
201
+ limit_level: NDArray[np.float64],
202
+ margin: NDArray[np.float64],
203
+ detector: DetectorType,
204
+ ) -> list[ComplianceViolation]:
205
+ """Find compliance violations.
215
206
 
216
- # Find violations
207
+ Args:
208
+ freq: Frequency array.
209
+ spectrum_level: Measured spectrum level.
210
+ limit_level: Limit level.
211
+ margin: Margin to limit (positive = passing).
212
+ detector: Detector type.
213
+
214
+ Returns:
215
+ List of violations.
216
+ """
217
217
  violations: list[ComplianceViolation] = []
218
218
  violation_mask = margin < 0
219
+
219
220
  if np.any(violation_mask):
220
221
  violation_indices = np.where(violation_mask)[0]
221
222
  for idx in violation_indices:
@@ -230,7 +231,68 @@ def check_compliance(
230
231
  )
231
232
  )
232
233
 
233
- # Overall results
234
+ return violations
235
+
236
+
237
+ def check_compliance(
238
+ trace_or_spectrum: WaveformTrace | tuple[NDArray[np.float64], NDArray[np.float64]],
239
+ mask: LimitMask,
240
+ *,
241
+ detector: DetectorType | str = DetectorType.PEAK,
242
+ frequency_range: tuple[float, float] | None = None,
243
+ unit_conversion: str | None = None,
244
+ ) -> ComplianceResult:
245
+ """Check signal against EMC limit mask.
246
+
247
+ Args:
248
+ trace_or_spectrum: Either a WaveformTrace to analyze, or a tuple of
249
+ (frequency_array, magnitude_array) if spectrum already computed.
250
+ mask: LimitMask to test against.
251
+ detector: Detector type to use ('peak', 'quasi-peak', 'average', 'rms').
252
+ frequency_range: Optional (min, max) frequency range to test.
253
+ unit_conversion: Optional unit conversion ('V_to_dBuV', 'W_to_dBm', etc.)
254
+
255
+ Returns:
256
+ ComplianceResult with pass/fail status and violation details.
257
+
258
+ Example:
259
+ >>> mask = load_limit_mask('FCC_Part15_ClassB')
260
+ >>> result = check_compliance(trace, mask)
261
+ >>> print(result.summary())
262
+ """
263
+ # Handle detector type
264
+ if isinstance(detector, str):
265
+ detector = DetectorType(detector.lower().replace("-", "_").replace(" ", "_"))
266
+
267
+ # Prepare spectrum
268
+ freq, spectrum_level = _prepare_spectrum(trace_or_spectrum, detector, unit_conversion)
269
+
270
+ # Apply frequency filters
271
+ freq, spectrum_level = _apply_frequency_filters(freq, spectrum_level, mask, frequency_range)
272
+
273
+ # Handle empty frequency range
274
+ if len(freq) == 0:
275
+ return ComplianceResult(
276
+ status="PASS",
277
+ mask_name=mask.name,
278
+ violations=[],
279
+ margin_to_limit=np.inf,
280
+ worst_frequency=0.0,
281
+ worst_margin=np.inf,
282
+ spectrum_freq=np.array([]),
283
+ spectrum_level=np.array([]),
284
+ limit_level=np.array([]),
285
+ detector=detector.value,
286
+ )
287
+
288
+ # Interpolate limit and calculate margin
289
+ limit_level = mask.interpolate(freq)
290
+ margin = limit_level - spectrum_level
291
+
292
+ # Find violations
293
+ violations = _find_violations(freq, spectrum_level, limit_level, margin, detector)
294
+
295
+ # Compute overall results
234
296
  status = "FAIL" if violations else "PASS"
235
297
  margin_to_limit = float(np.min(margin))
236
298
  worst_idx = int(np.argmin(margin))