oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,756 @@
1
+ """Advanced EMC compliance features.
2
+
3
+ This module provides advanced compliance testing capabilities including
4
+ limit interpolation, compliance test execution, and quasi-peak detection.
5
+
6
+
7
+ References:
8
+ CISPR 16-1-1: Measuring Apparatus
9
+ FCC Part 15: Unintentional Radiators
10
+ EN 55032: EMC Standard for Multimedia Equipment
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ import numpy as np
21
+
22
+ if TYPE_CHECKING:
23
+ from numpy.typing import NDArray
24
+
25
+ from oscura.compliance.masks import LimitMask
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ __all__ = [
30
+ "ComplianceTestConfig",
31
+ "ComplianceTestRunner",
32
+ "ComplianceTestSuite",
33
+ "InterpolationMethod",
34
+ "LimitInterpolator",
35
+ "QPDetectorBand",
36
+ "QuasiPeakDetector",
37
+ "interpolate_limit",
38
+ "run_compliance_suite",
39
+ ]
40
+
41
+
42
+ # =============================================================================
43
+ # =============================================================================
44
+
45
+
46
+ class InterpolationMethod(Enum):
47
+ """Interpolation methods for limit masks.
48
+
49
+ References:
50
+ COMP-005: Limit Interpolation
51
+ """
52
+
53
+ LINEAR = "linear" # Linear interpolation
54
+ LOG_LINEAR = "log-linear" # Log-linear (dB) interpolation
55
+ CUBIC = "cubic" # Cubic spline
56
+ STEP = "step" # Step function (no interpolation)
57
+
58
+
59
+ class LimitInterpolator:
60
+ """Limit mask interpolator.
61
+
62
+ Provides accurate interpolation of EMC limits between
63
+ defined frequency points.
64
+
65
+ Example:
66
+ >>> from oscura.compliance import load_limit_mask
67
+ >>> mask = load_limit_mask('FCC_Part15_ClassB')
68
+ >>> interp = LimitInterpolator(mask)
69
+ >>> limit_at_100mhz = interp.interpolate(100e6)
70
+
71
+ References:
72
+ COMP-005: Limit Interpolation
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ mask: LimitMask,
78
+ method: InterpolationMethod = InterpolationMethod.LOG_LINEAR,
79
+ extrapolate: bool = False,
80
+ ) -> None:
81
+ """Initialize interpolator.
82
+
83
+ Args:
84
+ mask: Limit mask to interpolate
85
+ method: Interpolation method
86
+ extrapolate: Allow extrapolation beyond mask range
87
+ """
88
+ self._mask = mask
89
+ self._method = method
90
+ self._extrapolate = extrapolate
91
+
92
+ # Precompute log frequencies for log-linear interpolation
93
+ self._log_freq = np.log10(mask.frequency)
94
+ self._log_limit = mask.limit # Already in dB
95
+
96
+ def interpolate(
97
+ self,
98
+ frequency: float | NDArray[np.float64],
99
+ ) -> NDArray[np.float64]:
100
+ """Interpolate limit at given frequency/frequencies.
101
+
102
+ Args:
103
+ frequency: Frequency or array of frequencies in Hz
104
+
105
+ Returns:
106
+ Interpolated limit value(s)
107
+
108
+ Raises:
109
+ ValueError: If frequency outside range and extrapolation disabled
110
+ """
111
+ freq_array = np.atleast_1d(np.asarray(frequency, dtype=np.float64))
112
+
113
+ # Validate positive frequencies first
114
+ if np.any(freq_array <= 0):
115
+ raise ValueError("Frequency must be positive")
116
+
117
+ # Check range
118
+ f_min, f_max = self._mask.frequency_range
119
+ if not self._extrapolate:
120
+ if np.any(freq_array < f_min) or np.any(freq_array > f_max):
121
+ out_of_range = freq_array[(freq_array < f_min) | (freq_array > f_max)]
122
+ raise ValueError(
123
+ f"Frequency {out_of_range[0]:.2e} Hz outside mask range "
124
+ f"[{f_min:.2e}, {f_max:.2e}] Hz. "
125
+ f"Set extrapolate=True to allow extrapolation."
126
+ )
127
+
128
+ if self._method == InterpolationMethod.LINEAR:
129
+ return self._interp_linear(freq_array)
130
+ elif self._method == InterpolationMethod.LOG_LINEAR:
131
+ return self._interp_log_linear(freq_array)
132
+ elif self._method == InterpolationMethod.CUBIC:
133
+ return self._interp_cubic(freq_array)
134
+ else: # STEP
135
+ return self._interp_step(freq_array)
136
+
137
+ def _interp_linear(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
138
+ """Linear interpolation."""
139
+ return np.interp(freq, self._mask.frequency, self._mask.limit)
140
+
141
+ def _interp_log_linear(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
142
+ """Log-linear interpolation (linear in log-frequency space)."""
143
+ log_freq = np.log10(freq)
144
+ return np.interp(log_freq, self._log_freq, self._log_limit)
145
+
146
+ def _interp_cubic(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
147
+ """Cubic spline interpolation."""
148
+ from scipy.interpolate import CubicSpline
149
+
150
+ # Use log-frequency for better behavior
151
+ log_freq = np.log10(freq)
152
+ spline = CubicSpline(self._log_freq, self._log_limit, extrapolate=self._extrapolate)
153
+ result: NDArray[np.float64] = spline(log_freq)
154
+ return result
155
+
156
+ def _interp_step(self, freq: NDArray[np.float64]) -> NDArray[np.float64]:
157
+ """Step function (nearest lower point)."""
158
+ result = np.zeros_like(freq)
159
+ for i, f in enumerate(freq):
160
+ idx = np.searchsorted(self._mask.frequency, f, side="right") - 1
161
+ idx = max(0, min(idx, len(self._mask.limit) - 1))
162
+ result[i] = self._mask.limit[idx]
163
+ return result
164
+
165
+ def get_limit_at(
166
+ self,
167
+ frequency: float,
168
+ warn_on_extrapolation: bool = True,
169
+ ) -> tuple[float, dict[str, Any]]:
170
+ """Get limit at specific frequency with metadata.
171
+
172
+ Args:
173
+ frequency: Frequency in Hz
174
+ warn_on_extrapolation: Emit warning if extrapolating
175
+
176
+ Returns:
177
+ (limit_value, metadata) tuple
178
+ """
179
+ f_min, f_max = self._mask.frequency_range
180
+ is_extrapolated = frequency < f_min or frequency > f_max
181
+
182
+ if is_extrapolated and warn_on_extrapolation:
183
+ logger.warning(
184
+ f"Extrapolating limit at {frequency:.2e} Hz "
185
+ f"(mask range: {f_min:.2e} to {f_max:.2e} Hz)"
186
+ )
187
+
188
+ limit = (
189
+ float(self.interpolate(frequency)[0])
190
+ if not is_extrapolated or self._extrapolate
191
+ else np.nan
192
+ )
193
+
194
+ # Find nearest defined points
195
+ idx = np.searchsorted(self._mask.frequency, frequency)
196
+ if idx == 0:
197
+ lower_freq = None
198
+ upper_freq = self._mask.frequency[0]
199
+ elif idx >= len(self._mask.frequency):
200
+ lower_freq = self._mask.frequency[-1]
201
+ upper_freq = None
202
+ else:
203
+ lower_freq = self._mask.frequency[idx - 1]
204
+ upper_freq = self._mask.frequency[idx]
205
+
206
+ return limit, {
207
+ "frequency": frequency,
208
+ "method": self._method.value,
209
+ "is_extrapolated": is_extrapolated,
210
+ "is_at_defined_point": frequency in self._mask.frequency,
211
+ "lower_defined_freq": float(lower_freq) if lower_freq is not None else None,
212
+ "upper_defined_freq": float(upper_freq) if upper_freq is not None else None,
213
+ }
214
+
215
+
216
+ def interpolate_limit(
217
+ mask: LimitMask,
218
+ frequency: float | NDArray[np.float64],
219
+ method: str = "log-linear",
220
+ ) -> NDArray[np.float64]:
221
+ """Convenience function for limit interpolation.
222
+
223
+ Args:
224
+ mask: Limit mask
225
+ frequency: Frequency or frequencies in Hz
226
+ method: Interpolation method
227
+
228
+ Returns:
229
+ Interpolated limit value(s)
230
+
231
+ Example:
232
+ >>> limit = interpolate_limit(mask, 100e6)
233
+ """
234
+ interp = LimitInterpolator(
235
+ mask,
236
+ method=InterpolationMethod(method),
237
+ extrapolate=True,
238
+ )
239
+ return interp.interpolate(frequency)
240
+
241
+
242
+ # =============================================================================
243
+ # =============================================================================
244
+
245
+
246
+ @dataclass
247
+ class ComplianceTestConfig:
248
+ """Configuration for compliance test.
249
+
250
+ Attributes:
251
+ mask_names: List of mask names to test against
252
+ detector_type: Detector type to use
253
+ frequency_range: Frequency range to test
254
+ margin_required_db: Required margin to limit
255
+ include_quasi_peak: Include QP detection
256
+ generate_report: Generate detailed report
257
+
258
+ References:
259
+ COMP-006: Compliance Test Execution
260
+ """
261
+
262
+ mask_names: list[str] = field(default_factory=lambda: ["FCC_Part15_ClassB"])
263
+ detector_type: str = "peak"
264
+ frequency_range: tuple[float, float] | None = None
265
+ margin_required_db: float = 0.0
266
+ include_quasi_peak: bool = True
267
+ generate_report: bool = True
268
+
269
+
270
+ @dataclass
271
+ class ComplianceTestResult:
272
+ """Result of a single compliance test.
273
+
274
+ Attributes:
275
+ mask_name: Mask tested against
276
+ passed: Whether test passed
277
+ margin_db: Margin to limit (negative = fail)
278
+ worst_frequency: Worst-case frequency
279
+ violations: List of violations
280
+ detector_used: Detector type used
281
+ """
282
+
283
+ mask_name: str
284
+ passed: bool
285
+ margin_db: float
286
+ worst_frequency: float
287
+ violations: list[dict[str, Any]]
288
+ detector_used: str
289
+ metadata: dict[str, Any] = field(default_factory=dict)
290
+
291
+
292
+ @dataclass
293
+ class ComplianceTestSuiteResult:
294
+ """Result of compliance test suite.
295
+
296
+ Attributes:
297
+ overall_passed: True if all tests passed
298
+ results: Individual test results
299
+ summary: Test summary
300
+ """
301
+
302
+ overall_passed: bool
303
+ results: list[ComplianceTestResult]
304
+ summary: dict[str, Any]
305
+
306
+
307
+ class ComplianceTestRunner:
308
+ """Compliance test execution engine.
309
+
310
+ Executes compliance tests against multiple masks with
311
+ configurable detection methods.
312
+
313
+ Example:
314
+ >>> runner = ComplianceTestRunner()
315
+ >>> runner.add_mask('FCC_Part15_ClassB')
316
+ >>> runner.add_mask('CE_CISPR32_ClassB')
317
+ >>> result = runner.run(spectrum_freq, spectrum_level)
318
+
319
+ References:
320
+ COMP-006: Compliance Test Execution
321
+ """
322
+
323
+ def __init__(self, config: ComplianceTestConfig | None = None) -> None:
324
+ """Initialize test runner.
325
+
326
+ Args:
327
+ config: Test configuration
328
+ """
329
+ self._config = config or ComplianceTestConfig()
330
+ self._masks: list[tuple[str, Any]] = []
331
+ self._qp_detector = QuasiPeakDetector()
332
+
333
+ def add_mask(self, mask_name: str) -> ComplianceTestRunner:
334
+ """Add mask to test suite.
335
+
336
+ Args:
337
+ mask_name: Mask name to add
338
+
339
+ Returns:
340
+ Self for chaining
341
+ """
342
+ from oscura.compliance.masks import load_limit_mask
343
+
344
+ mask = load_limit_mask(mask_name)
345
+ self._masks.append((mask_name, mask))
346
+ return self
347
+
348
+ def run(
349
+ self,
350
+ frequencies: NDArray[np.float64],
351
+ levels: NDArray[np.float64],
352
+ unit: str = "dBuV",
353
+ ) -> ComplianceTestSuiteResult:
354
+ """Run compliance test suite.
355
+
356
+ Args:
357
+ frequencies: Frequency array in Hz
358
+ levels: Level array in specified unit
359
+ unit: Unit of level measurements
360
+
361
+ Returns:
362
+ Test suite result
363
+ """
364
+ results: list[ComplianceTestResult] = []
365
+
366
+ for _mask_name, mask in self._masks:
367
+ result = self._test_against_mask(frequencies, levels, mask, unit)
368
+ results.append(result)
369
+
370
+ overall_passed = all(r.passed for r in results)
371
+
372
+ summary = {
373
+ "total_tests": len(results),
374
+ "passed": sum(1 for r in results if r.passed),
375
+ "failed": sum(1 for r in results if not r.passed),
376
+ "worst_margin_db": min(r.margin_db for r in results) if results else 0,
377
+ "masks_tested": [r.mask_name for r in results],
378
+ }
379
+
380
+ return ComplianceTestSuiteResult(
381
+ overall_passed=overall_passed,
382
+ results=results,
383
+ summary=summary,
384
+ )
385
+
386
+ def _test_against_mask(
387
+ self,
388
+ frequencies: NDArray[np.float64],
389
+ levels: NDArray[np.float64],
390
+ mask: Any,
391
+ unit: str,
392
+ ) -> ComplianceTestResult:
393
+ """Test against single mask."""
394
+ # Apply frequency range filter
395
+ if self._config.frequency_range:
396
+ f_min, f_max = self._config.frequency_range
397
+ in_range = (frequencies >= f_min) & (frequencies <= f_max)
398
+ frequencies = frequencies[in_range]
399
+ levels = levels[in_range]
400
+
401
+ # Limit to mask range
402
+ mask_f_min, mask_f_max = mask.frequency_range
403
+ in_mask = (frequencies >= mask_f_min) & (frequencies <= mask_f_max)
404
+ frequencies = frequencies[in_mask]
405
+ levels = levels[in_mask]
406
+
407
+ if len(frequencies) == 0:
408
+ return ComplianceTestResult(
409
+ mask_name=mask.name,
410
+ passed=True,
411
+ margin_db=np.inf,
412
+ worst_frequency=0.0,
413
+ violations=[],
414
+ detector_used=self._config.detector_type,
415
+ )
416
+
417
+ # Interpolate limits
418
+ interp = LimitInterpolator(mask)
419
+ limits = interp.interpolate(frequencies)
420
+
421
+ # Apply quasi-peak if requested
422
+ if self._config.include_quasi_peak and mask.detector == "quasi-peak":
423
+ levels = self._qp_detector.apply(levels, frequencies)
424
+
425
+ # Calculate margin
426
+ margin = limits - levels
427
+ min_margin = float(np.min(margin))
428
+ worst_idx = int(np.argmin(margin))
429
+
430
+ # Find violations (considering required margin)
431
+ violations = []
432
+ violation_mask = margin < self._config.margin_required_db
433
+ if np.any(violation_mask):
434
+ for idx in np.where(violation_mask)[0]:
435
+ violations.append(
436
+ {
437
+ "frequency": float(frequencies[idx]),
438
+ "measured": float(levels[idx]),
439
+ "limit": float(limits[idx]),
440
+ "excess_db": float(-margin[idx]),
441
+ }
442
+ )
443
+
444
+ passed = len(violations) == 0
445
+
446
+ return ComplianceTestResult(
447
+ mask_name=mask.name,
448
+ passed=passed,
449
+ margin_db=min_margin,
450
+ worst_frequency=float(frequencies[worst_idx]),
451
+ violations=violations,
452
+ detector_used=self._config.detector_type,
453
+ metadata={"unit": unit},
454
+ )
455
+
456
+
457
+ class ComplianceTestSuite:
458
+ """Pre-configured compliance test suites.
459
+
460
+ Provides standard test configurations for common scenarios.
461
+
462
+ References:
463
+ COMP-006: Compliance Test Execution
464
+ """
465
+
466
+ @staticmethod
467
+ def residential() -> ComplianceTestRunner:
468
+ """Get residential (Class B) test suite."""
469
+ runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=True))
470
+ runner.add_mask("FCC_Part15_ClassB")
471
+ runner.add_mask("CE_CISPR32_ClassB")
472
+ return runner
473
+
474
+ @staticmethod
475
+ def commercial() -> ComplianceTestRunner:
476
+ """Get commercial (Class A) test suite."""
477
+ runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=True))
478
+ runner.add_mask("FCC_Part15_ClassA")
479
+ runner.add_mask("CE_CISPR32_ClassA")
480
+ return runner
481
+
482
+ @staticmethod
483
+ def military() -> ComplianceTestRunner:
484
+ """Get military (MIL-STD) test suite."""
485
+ runner = ComplianceTestRunner(ComplianceTestConfig(include_quasi_peak=False))
486
+ runner.add_mask("MIL_STD_461G_RE102")
487
+ runner.add_mask("MIL_STD_461G_CE102")
488
+ return runner
489
+
490
+
491
+ def run_compliance_suite(
492
+ frequencies: NDArray[np.float64],
493
+ levels: NDArray[np.float64],
494
+ suite: str = "residential",
495
+ ) -> ComplianceTestSuiteResult:
496
+ """Run standard compliance test suite.
497
+
498
+ Args:
499
+ frequencies: Frequency array in Hz
500
+ levels: Level array in dB
501
+ suite: Suite name ('residential', 'commercial', 'military')
502
+
503
+ Returns:
504
+ Test suite result
505
+
506
+ Raises:
507
+ ValueError: If suite name is unknown.
508
+
509
+ Example:
510
+ >>> result = run_compliance_suite(freq, levels, suite='residential')
511
+ >>> print(f"Passed: {result.overall_passed}")
512
+ """
513
+ if suite == "residential":
514
+ runner = ComplianceTestSuite.residential()
515
+ elif suite == "commercial":
516
+ runner = ComplianceTestSuite.commercial()
517
+ elif suite == "military":
518
+ runner = ComplianceTestSuite.military()
519
+ else:
520
+ raise ValueError(f"Unknown suite: {suite}")
521
+
522
+ return runner.run(frequencies, levels)
523
+
524
+
525
+ # =============================================================================
526
+ # =============================================================================
527
+
528
+
529
+ class QPDetectorBand(Enum):
530
+ """CISPR 16-1-1 quasi-peak detector bands.
531
+
532
+ References:
533
+ CISPR 16-1-1 Table 1
534
+ COMP-007: Quasi-Peak Detection
535
+ """
536
+
537
+ BAND_A = "A" # 9 kHz - 150 kHz
538
+ BAND_B = "B" # 150 kHz - 30 MHz
539
+ BAND_C = "C" # 30 MHz - 300 MHz
540
+ BAND_D = "D" # 300 MHz - 1 GHz
541
+
542
+
543
+ @dataclass
544
+ class QPDetectorParams:
545
+ """Quasi-peak detector parameters per CISPR 16-1-1.
546
+
547
+ Attributes:
548
+ bandwidth: Measurement bandwidth in Hz
549
+ charge_time: Charge time constant in ms
550
+ discharge_time: Discharge time constant in ms
551
+ mechanical_time: Meter mechanical time constant in ms
552
+ """
553
+
554
+ bandwidth: float
555
+ charge_time: float
556
+ discharge_time: float
557
+ mechanical_time: float
558
+
559
+
560
+ class QuasiPeakDetector:
561
+ """CISPR 16-1-1 quasi-peak detector.
562
+
563
+ Implements quasi-peak detection per CISPR 16-1-1 standard for
564
+ EMC compliance measurements.
565
+
566
+ Example:
567
+ >>> detector = QuasiPeakDetector()
568
+ >>> qp_levels = detector.apply(peak_levels, frequencies)
569
+
570
+ References:
571
+ CISPR 16-1-1: Measuring Apparatus
572
+ COMP-007: Quasi-Peak Detection
573
+ """
574
+
575
+ # CISPR 16-1-1 detector parameters by band
576
+ BAND_PARAMS = { # noqa: RUF012
577
+ QPDetectorBand.BAND_A: QPDetectorParams(
578
+ bandwidth=200, # 200 Hz
579
+ charge_time=45, # ms
580
+ discharge_time=500, # ms
581
+ mechanical_time=160, # ms
582
+ ),
583
+ QPDetectorBand.BAND_B: QPDetectorParams(
584
+ bandwidth=9000, # 9 kHz
585
+ charge_time=1, # ms
586
+ discharge_time=160, # ms
587
+ mechanical_time=160, # ms
588
+ ),
589
+ QPDetectorBand.BAND_C: QPDetectorParams(
590
+ bandwidth=120000, # 120 kHz
591
+ charge_time=1, # ms
592
+ discharge_time=550, # ms
593
+ mechanical_time=100, # ms
594
+ ),
595
+ QPDetectorBand.BAND_D: QPDetectorParams(
596
+ bandwidth=1000000, # 1 MHz
597
+ charge_time=1, # ms
598
+ discharge_time=550, # ms
599
+ mechanical_time=100, # ms
600
+ ),
601
+ }
602
+
603
+ # Frequency ranges for bands (Hz)
604
+ BAND_RANGES = { # noqa: RUF012
605
+ QPDetectorBand.BAND_A: (9e3, 150e3),
606
+ QPDetectorBand.BAND_B: (150e3, 30e6),
607
+ QPDetectorBand.BAND_C: (30e6, 300e6),
608
+ QPDetectorBand.BAND_D: (300e6, 1e9),
609
+ }
610
+
611
+ def __init__(self) -> None:
612
+ """Initialize quasi-peak detector."""
613
+ self._lookup_table: dict[str, NDArray[np.float64]] = {}
614
+
615
+ def get_band(self, frequency: float) -> QPDetectorBand | None:
616
+ """Get CISPR band for frequency.
617
+
618
+ Args:
619
+ frequency: Frequency in Hz
620
+
621
+ Returns:
622
+ Band or None if outside all bands
623
+ """
624
+ for band, (f_min, f_max) in self.BAND_RANGES.items():
625
+ if f_min <= frequency <= f_max:
626
+ return band
627
+ return None
628
+
629
+ def get_params(self, frequency: float) -> QPDetectorParams | None:
630
+ """Get detector parameters for frequency.
631
+
632
+ Args:
633
+ frequency: Frequency in Hz
634
+
635
+ Returns:
636
+ Detector parameters or None
637
+ """
638
+ band = self.get_band(frequency)
639
+ if band is None:
640
+ return None
641
+ return self.BAND_PARAMS[band]
642
+
643
+ def apply(
644
+ self,
645
+ peak_levels: NDArray[np.float64],
646
+ frequencies: NDArray[np.float64],
647
+ ) -> NDArray[np.float64]:
648
+ """Apply quasi-peak detection to peak levels.
649
+
650
+ Args:
651
+ peak_levels: Peak detector levels in dB
652
+ frequencies: Corresponding frequencies in Hz
653
+
654
+ Returns:
655
+ Quasi-peak levels in dB
656
+
657
+ Note:
658
+ Quasi-peak is always <= peak for repetitive signals.
659
+ The correction factor depends on pulse repetition rate.
660
+ """
661
+ qp_levels = np.copy(peak_levels)
662
+
663
+ for i, (level, freq) in enumerate(zip(peak_levels, frequencies, strict=False)):
664
+ band = self.get_band(freq)
665
+ if band is not None:
666
+ # Apply approximate QP correction
667
+ # Real implementation would need actual signal for time-domain processing
668
+ correction = self._get_qp_correction(band)
669
+ qp_levels[i] = level - correction
670
+
671
+ return qp_levels
672
+
673
+ def _get_qp_correction(self, band: QPDetectorBand) -> float:
674
+ """Get approximate QP correction factor.
675
+
676
+ This is a simplified model. Real QP detection requires
677
+ time-domain processing of the actual signal.
678
+
679
+ Args:
680
+ band: CISPR band
681
+
682
+ Returns:
683
+ Correction factor in dB
684
+ """
685
+ # Approximate corrections for periodic signals
686
+ # Actual correction depends on pulse rate and duty cycle
687
+ corrections = {
688
+ QPDetectorBand.BAND_A: 3.0,
689
+ QPDetectorBand.BAND_B: 6.0,
690
+ QPDetectorBand.BAND_C: 4.0,
691
+ QPDetectorBand.BAND_D: 4.0,
692
+ }
693
+ return corrections.get(band, 0.0)
694
+
695
+ def compare_peak_qp(
696
+ self,
697
+ peak_levels: NDArray[np.float64],
698
+ frequencies: NDArray[np.float64],
699
+ ) -> dict[str, Any]:
700
+ """Compare peak and quasi-peak readings.
701
+
702
+ Args:
703
+ peak_levels: Peak detector levels
704
+ frequencies: Frequencies
705
+
706
+ Returns:
707
+ Comparison results
708
+ """
709
+ qp_levels = self.apply(peak_levels, frequencies)
710
+ difference = peak_levels - qp_levels
711
+
712
+ return {
713
+ "peak_levels": peak_levels,
714
+ "qp_levels": qp_levels,
715
+ "difference_db": difference,
716
+ "max_difference_db": float(np.max(difference)),
717
+ "avg_difference_db": float(np.mean(difference)),
718
+ "description": (
719
+ "Quasi-peak is lower than peak for pulsed/repetitive signals. "
720
+ "For CW signals, QP equals peak."
721
+ ),
722
+ }
723
+
724
+ def get_bandwidth(self, frequency: float) -> float:
725
+ """Get measurement bandwidth for frequency.
726
+
727
+ Args:
728
+ frequency: Frequency in Hz
729
+
730
+ Returns:
731
+ Bandwidth in Hz
732
+ """
733
+ params = self.get_params(frequency)
734
+ if params is None:
735
+ # Default to Band B
736
+ return 9000
737
+ return params.bandwidth
738
+
739
+ def validate_bandwidth(self, bandwidth: float) -> None:
740
+ """Validate measurement bandwidth.
741
+
742
+ Args:
743
+ bandwidth: Bandwidth to validate
744
+
745
+ Raises:
746
+ ValueError: If bandwidth is invalid
747
+ """
748
+ if bandwidth <= 0:
749
+ raise ValueError("Bandwidth must be positive")
750
+
751
+ valid_bandwidths = [p.bandwidth for p in self.BAND_PARAMS.values()]
752
+ if bandwidth not in valid_bandwidths:
753
+ logger.warning(
754
+ f"Non-standard bandwidth {bandwidth} Hz. "
755
+ f"Standard CISPR bandwidths: {valid_bandwidths}"
756
+ )