oscura 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,298 @@
1
+ """EMC compliance testing implementation.
2
+
3
+ This module provides compliance testing against regulatory limit masks.
4
+
5
+
6
+ Example:
7
+ >>> from oscura.compliance import load_limit_mask, test_compliance
8
+ >>> mask = load_limit_mask('FCC_Part15_ClassB')
9
+ >>> result = test_compliance(trace, mask)
10
+ >>> print(f"Status: {result.status}")
11
+
12
+ References:
13
+ CISPR 16-1-1 (Measuring Apparatus)
14
+ ANSI C63.2 (Instrumentation)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from typing import TYPE_CHECKING, Any
22
+
23
+ import numpy as np
24
+
25
+ if TYPE_CHECKING:
26
+ from numpy.typing import NDArray
27
+
28
+ from oscura.compliance.masks import LimitMask
29
+ from oscura.core.types import WaveformTrace
30
+
31
+
32
+ class DetectorType(Enum):
33
+ """EMC measurement detector types."""
34
+
35
+ PEAK = "peak"
36
+ QUASI_PEAK = "quasi-peak"
37
+ AVERAGE = "average"
38
+ RMS = "rms"
39
+
40
+
41
+ @dataclass
42
+ class ComplianceViolation:
43
+ """Single compliance violation record.
44
+
45
+ Attributes:
46
+ frequency: Violation frequency in Hz
47
+ measured_level: Measured level in mask unit (dBuV, etc.)
48
+ limit_level: Limit level at this frequency
49
+ excess_db: Amount exceeding limit (positive = violation)
50
+ detector: Detector type used
51
+ severity: Severity classification
52
+ """
53
+
54
+ frequency: float
55
+ measured_level: float
56
+ limit_level: float
57
+ excess_db: float
58
+ detector: str = "peak"
59
+ severity: str = "FAIL"
60
+
61
+ def __str__(self) -> str:
62
+ """Format violation as string."""
63
+ freq_mhz = self.frequency / 1e6
64
+ return (
65
+ f"{freq_mhz:.3f} MHz: {self.measured_level:.1f} dB "
66
+ f"(limit: {self.limit_level:.1f} dB, excess: {self.excess_db:.1f} dB)"
67
+ )
68
+
69
+
70
+ @dataclass
71
+ class ComplianceResult:
72
+ """Compliance test result.
73
+
74
+ Attributes:
75
+ status: Overall status ('PASS' or 'FAIL')
76
+ mask_name: Name of limit mask used
77
+ violations: List of violations
78
+ margin_to_limit: Minimum margin in dB (negative = failing)
79
+ worst_frequency: Frequency with worst margin
80
+ worst_margin: Worst margin value in dB
81
+ spectrum_freq: Tested frequency array
82
+ spectrum_level: Measured level array
83
+ limit_level: Limit level array (interpolated to spectrum frequencies)
84
+ detector: Detector type used
85
+ metadata: Additional result metadata
86
+ """
87
+
88
+ status: str
89
+ mask_name: str
90
+ violations: list[ComplianceViolation]
91
+ margin_to_limit: float
92
+ worst_frequency: float
93
+ worst_margin: float
94
+ spectrum_freq: NDArray[np.float64]
95
+ spectrum_level: NDArray[np.float64]
96
+ limit_level: NDArray[np.float64]
97
+ detector: str = "peak"
98
+ metadata: dict[str, Any] = field(default_factory=dict)
99
+
100
+ @property
101
+ def passed(self) -> bool:
102
+ """Return True if compliance test passed."""
103
+ return self.status == "PASS"
104
+
105
+ @property
106
+ def violation_count(self) -> int:
107
+ """Return number of violations."""
108
+ return len(self.violations)
109
+
110
+ def summary(self) -> str:
111
+ """Generate text summary of result."""
112
+ lines = [
113
+ f"EMC Compliance Test: {self.mask_name}",
114
+ f"Status: {self.status}",
115
+ f"Margin to limit: {self.margin_to_limit:.1f} dB",
116
+ f"Worst frequency: {self.worst_frequency / 1e6:.3f} MHz",
117
+ f"Worst margin: {self.worst_margin:.1f} dB",
118
+ ]
119
+
120
+ if self.violations:
121
+ lines.append(f"\nViolations ({len(self.violations)}):")
122
+ for v in self.violations[:10]: # Limit to first 10
123
+ lines.append(f" - {v}")
124
+ if len(self.violations) > 10:
125
+ lines.append(f" ... and {len(self.violations) - 10} more")
126
+
127
+ return "\n".join(lines)
128
+
129
+
130
+ def check_compliance(
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.
139
+
140
+ 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.)
147
+
148
+ 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())
155
+ """
156
+ from oscura.core.types import WaveformTrace
157
+
158
+ # Handle detector type
159
+ if isinstance(detector, str):
160
+ detector = DetectorType(detector.lower().replace("-", "_").replace(" ", "_"))
161
+
162
+ # Get spectrum
163
+ if isinstance(trace_or_spectrum, WaveformTrace):
164
+ freq, mag = _compute_spectrum(trace_or_spectrum, detector)
165
+ else:
166
+ freq, mag = trace_or_spectrum
167
+
168
+ # Convert to dB if needed
169
+ if unit_conversion == "V_to_dBuV":
170
+ # dBuV = 20*log10(V * 1e6)
171
+ spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
172
+ elif unit_conversion == "W_to_dBm":
173
+ # dBm = 10*log10(W * 1000)
174
+ spectrum_level = 10 * np.log10(np.abs(mag) * 1000 + 1e-12)
175
+ elif mag.max() > 0 and mag.max() < 10:
176
+ # Assume linear voltage, convert to dBuV
177
+ spectrum_level = 20 * np.log10(np.abs(mag) * 1e6 + 1e-12)
178
+ else:
179
+ # Assume already in dB
180
+ spectrum_level = mag
181
+
182
+ # Apply frequency range filter
183
+ if frequency_range is not None:
184
+ f_min, f_max = frequency_range
185
+ mask_filter = (freq >= f_min) & (freq <= f_max)
186
+ freq = freq[mask_filter]
187
+ spectrum_level = spectrum_level[mask_filter]
188
+
189
+ # Limit to mask frequency range
190
+ mask_f_min, mask_f_max = mask.frequency_range
191
+ in_range = (freq >= mask_f_min) & (freq <= mask_f_max)
192
+ freq = freq[in_range]
193
+ spectrum_level = spectrum_level[in_range]
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
+ )
209
+
210
+ # Interpolate limit to spectrum frequencies
211
+ limit_level = mask.interpolate(freq)
212
+
213
+ # Calculate margin (positive = passing)
214
+ margin = limit_level - spectrum_level
215
+
216
+ # Find violations
217
+ violations: list[ComplianceViolation] = []
218
+ violation_mask = margin < 0
219
+ if np.any(violation_mask):
220
+ violation_indices = np.where(violation_mask)[0]
221
+ for idx in violation_indices:
222
+ violations.append(
223
+ ComplianceViolation(
224
+ frequency=float(freq[idx]),
225
+ measured_level=float(spectrum_level[idx]),
226
+ limit_level=float(limit_level[idx]),
227
+ excess_db=float(-margin[idx]),
228
+ detector=detector.value,
229
+ severity="FAIL",
230
+ )
231
+ )
232
+
233
+ # Overall results
234
+ status = "FAIL" if violations else "PASS"
235
+ margin_to_limit = float(np.min(margin))
236
+ worst_idx = int(np.argmin(margin))
237
+ worst_frequency = float(freq[worst_idx])
238
+ worst_margin = float(margin[worst_idx])
239
+
240
+ return ComplianceResult(
241
+ status=status,
242
+ mask_name=mask.name,
243
+ violations=violations,
244
+ margin_to_limit=margin_to_limit,
245
+ worst_frequency=worst_frequency,
246
+ worst_margin=worst_margin,
247
+ spectrum_freq=freq,
248
+ spectrum_level=spectrum_level,
249
+ limit_level=limit_level,
250
+ detector=detector.value,
251
+ metadata={
252
+ "unit": mask.unit,
253
+ "distance": mask.distance,
254
+ "regulatory_body": mask.regulatory_body,
255
+ },
256
+ )
257
+
258
+
259
+ def _compute_spectrum(
260
+ trace: WaveformTrace,
261
+ detector: DetectorType,
262
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
263
+ """Compute spectrum from trace with specified detector.
264
+
265
+ Args:
266
+ trace: Input waveform trace.
267
+ detector: Detector type.
268
+
269
+ Returns:
270
+ (frequency, magnitude) arrays.
271
+ """
272
+ from oscura.analyzers.waveform.spectral import fft, psd
273
+
274
+ if detector == DetectorType.PEAK:
275
+ # Use FFT for peak detection
276
+ freq, mag = fft(trace) # type: ignore[misc]
277
+ return freq, np.abs(mag)
278
+ elif detector == DetectorType.AVERAGE:
279
+ # Use Welch PSD for averaging
280
+ freq, mag = psd(trace, method="welch") # type: ignore[call-arg]
281
+ return freq, np.sqrt(mag) # Convert PSD to magnitude
282
+ elif detector == DetectorType.QUASI_PEAK:
283
+ # Quasi-peak requires special weighting (simplified here)
284
+ # Real implementation would use CISPR 16 weighting network
285
+ freq, mag = fft(trace) # type: ignore[misc]
286
+ # Apply simplified quasi-peak envelope
287
+ return freq, np.abs(mag) * 0.8 # Approximate QP < peak
288
+ else: # RMS
289
+ freq, mag = psd(trace, method="welch") # type: ignore[call-arg]
290
+ return freq, np.sqrt(mag)
291
+
292
+
293
+ __all__ = [
294
+ "ComplianceResult",
295
+ "ComplianceViolation",
296
+ "DetectorType",
297
+ "check_compliance",
298
+ ]
@@ -0,0 +1,38 @@
1
+ """Component analysis module for Oscura.
2
+
3
+ This module provides TDR-based impedance extraction, capacitance/inductance
4
+ measurement, parasitic extraction, and transmission line analysis.
5
+ """
6
+
7
+ from oscura.component.impedance import (
8
+ discontinuity_analysis,
9
+ extract_impedance,
10
+ impedance_profile,
11
+ )
12
+ from oscura.component.reactive import (
13
+ extract_parasitics,
14
+ measure_capacitance,
15
+ measure_inductance,
16
+ )
17
+ from oscura.component.transmission_line import (
18
+ characteristic_impedance,
19
+ propagation_delay,
20
+ transmission_line_analysis,
21
+ velocity_factor,
22
+ )
23
+
24
+ __all__ = [
25
+ "characteristic_impedance",
26
+ "discontinuity_analysis",
27
+ # Impedance
28
+ "extract_impedance",
29
+ "extract_parasitics",
30
+ "impedance_profile",
31
+ # Reactive
32
+ "measure_capacitance",
33
+ "measure_inductance",
34
+ "propagation_delay",
35
+ # Transmission line
36
+ "transmission_line_analysis",
37
+ "velocity_factor",
38
+ ]
@@ -0,0 +1,365 @@
1
+ """TDR impedance extraction for Oscura.
2
+
3
+ This module provides impedance extraction from Time Domain Reflectometry
4
+ (TDR) measurements, including impedance profiling and discontinuity analysis.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.component import extract_impedance
9
+ >>> z0, z_profile = extract_impedance(tdr_trace)
10
+
11
+ References:
12
+ IPC-TM-650 2.5.5.7: Characteristic Impedance of Lines on PCBs
13
+ IEEE 370-2020: Electrical Characterization of Interconnects
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import TYPE_CHECKING, Literal
20
+
21
+ import numpy as np
22
+ from scipy import signal as sp_signal
23
+
24
+ from oscura.core.exceptions import InsufficientDataError
25
+
26
+ if TYPE_CHECKING:
27
+ from numpy.typing import NDArray
28
+
29
+ from oscura.core.types import WaveformTrace
30
+
31
+
32
+ @dataclass
33
+ class ImpedanceProfile:
34
+ """Impedance profile from TDR measurement.
35
+
36
+ Attributes:
37
+ distance: Distance axis in meters.
38
+ time: Time axis in seconds.
39
+ impedance: Impedance values in ohms.
40
+ z0_source: Source impedance (reference).
41
+ velocity: Propagation velocity used (m/s).
42
+ statistics: Additional statistics.
43
+ """
44
+
45
+ distance: NDArray[np.float64]
46
+ time: NDArray[np.float64]
47
+ impedance: NDArray[np.float64]
48
+ z0_source: float
49
+ velocity: float
50
+ statistics: dict = field(default_factory=dict) # type: ignore[type-arg]
51
+
52
+ @property
53
+ def mean_impedance(self) -> float:
54
+ """Mean impedance value."""
55
+ return float(np.mean(self.impedance))
56
+
57
+ @property
58
+ def max_impedance(self) -> float:
59
+ """Maximum impedance value."""
60
+ return float(np.max(self.impedance))
61
+
62
+ @property
63
+ def min_impedance(self) -> float:
64
+ """Minimum impedance value."""
65
+ return float(np.min(self.impedance))
66
+
67
+
68
+ @dataclass
69
+ class Discontinuity:
70
+ """A detected impedance discontinuity.
71
+
72
+ Attributes:
73
+ position: Position in meters.
74
+ time: Time position in seconds.
75
+ impedance_before: Impedance before discontinuity.
76
+ impedance_after: Impedance after discontinuity.
77
+ magnitude: Magnitude of change (ohms).
78
+ reflection_coeff: Reflection coefficient (rho).
79
+ discontinuity_type: Type of discontinuity.
80
+ """
81
+
82
+ position: float
83
+ time: float
84
+ impedance_before: float
85
+ impedance_after: float
86
+ magnitude: float
87
+ reflection_coeff: float
88
+ discontinuity_type: Literal["capacitive", "inductive", "resistive", "unknown"]
89
+
90
+
91
+ def extract_impedance(
92
+ trace: WaveformTrace,
93
+ *,
94
+ z0_source: float = 50.0,
95
+ velocity: float | None = None,
96
+ velocity_factor: float = 0.66,
97
+ start_time: float | None = None,
98
+ end_time: float | None = None,
99
+ ) -> tuple[float, ImpedanceProfile]:
100
+ """Extract impedance profile from TDR waveform.
101
+
102
+ Calculates the impedance profile from a TDR reflection measurement
103
+ using the relationship between incident and reflected waves.
104
+
105
+ Args:
106
+ trace: TDR reflection waveform.
107
+ z0_source: Source/reference impedance (default 50 ohms).
108
+ velocity: Propagation velocity in m/s. If None, calculated from
109
+ velocity_factor.
110
+ velocity_factor: Fraction of speed of light (default 0.66 for FR4).
111
+ start_time: Start time for analysis window (seconds).
112
+ end_time: End time for analysis window (seconds).
113
+
114
+ Returns:
115
+ Tuple of (characteristic_impedance, impedance_profile).
116
+
117
+ Raises:
118
+ InsufficientDataError: If trace has fewer than 10 samples.
119
+
120
+ Example:
121
+ >>> z0, profile = extract_impedance(tdr_trace, z0_source=50)
122
+ >>> print(f"Z0 = {z0:.1f} ohms")
123
+
124
+ References:
125
+ IPC-TM-650 2.5.5.7
126
+ """
127
+ data = trace.data.astype(np.float64)
128
+ sample_rate = trace.metadata.sample_rate
129
+ dt = 1.0 / sample_rate
130
+
131
+ if len(data) < 10:
132
+ raise InsufficientDataError(
133
+ "TDR analysis requires at least 10 samples",
134
+ required=10,
135
+ available=len(data),
136
+ analysis_type="tdr_impedance",
137
+ )
138
+
139
+ # Calculate propagation velocity
140
+ c = 299792458.0 # Speed of light in m/s
141
+ if velocity is None:
142
+ velocity = c * velocity_factor
143
+
144
+ # Create time and distance axes
145
+ time_axis = np.arange(len(data)) * dt
146
+ # TDR: distance is velocity * time / 2 (round trip)
147
+ distance_axis = velocity * time_axis / 2.0
148
+
149
+ # Apply time window if specified
150
+ start_idx = 0
151
+ end_idx = len(data)
152
+ if start_time is not None:
153
+ start_idx = int(start_time * sample_rate)
154
+ if end_time is not None:
155
+ end_idx = int(end_time * sample_rate)
156
+
157
+ start_idx = max(0, min(start_idx, len(data) - 1))
158
+ end_idx = max(start_idx + 1, min(end_idx, len(data)))
159
+
160
+ # Find the incident step level in TDR data
161
+ # For TDR with a matched load (Z = Z0), the steady-state voltage is V_source/2
162
+ incident_level = _find_incident_level(data)
163
+
164
+ # Calculate reflection coefficient from TDR waveform
165
+ # For TDR: V_measured = V_incident * (1 + rho)
166
+ # where rho is the reflection coefficient
167
+ # So: rho = (V_measured / V_incident) - 1
168
+
169
+ if incident_level > 0:
170
+ rho = (data / incident_level) - 1.0
171
+ else:
172
+ # Fallback: assume data is already normalized
173
+ rho = data - 1.0
174
+
175
+ # Calculate impedance from reflection coefficient
176
+ # Z = Z0 * (1 + rho) / (1 - rho)
177
+ with np.errstate(divide="ignore", invalid="ignore"):
178
+ impedance = z0_source * (1 + rho) / (1 - rho)
179
+ # Clip unreasonable values
180
+ impedance = np.clip(impedance, 1.0, 10000.0)
181
+
182
+ # Extract characteristic impedance from stable region
183
+ stable_region = impedance[start_idx:end_idx]
184
+ z0 = float(np.median(stable_region))
185
+
186
+ # Create profile
187
+ profile = ImpedanceProfile(
188
+ distance=distance_axis,
189
+ time=time_axis,
190
+ impedance=impedance,
191
+ z0_source=z0_source,
192
+ velocity=velocity,
193
+ statistics={
194
+ "z0_measured": z0,
195
+ "z0_std": float(np.std(stable_region)),
196
+ "z0_min": float(np.min(stable_region)),
197
+ "z0_max": float(np.max(stable_region)),
198
+ "analysis_start_m": float(distance_axis[start_idx]),
199
+ "analysis_end_m": float(distance_axis[end_idx - 1]),
200
+ },
201
+ )
202
+
203
+ return z0, profile
204
+
205
+
206
+ def impedance_profile(
207
+ trace: WaveformTrace,
208
+ *,
209
+ z0_source: float = 50.0,
210
+ velocity_factor: float = 0.66,
211
+ smooth_window: int = 0,
212
+ ) -> ImpedanceProfile:
213
+ """Get impedance profile from TDR waveform.
214
+
215
+ Convenience function that returns just the impedance profile.
216
+
217
+ Args:
218
+ trace: TDR reflection waveform.
219
+ z0_source: Source/reference impedance.
220
+ velocity_factor: Fraction of speed of light.
221
+ smooth_window: Smoothing window size (0 = no smoothing).
222
+
223
+ Returns:
224
+ ImpedanceProfile object.
225
+ """
226
+ _, profile = extract_impedance(
227
+ trace,
228
+ z0_source=z0_source,
229
+ velocity_factor=velocity_factor,
230
+ )
231
+
232
+ if smooth_window > 0:
233
+ # Apply smoothing
234
+ kernel = np.ones(smooth_window) / smooth_window
235
+ profile.impedance = np.convolve(profile.impedance, kernel, mode="same")
236
+
237
+ return profile
238
+
239
+
240
+ def discontinuity_analysis(
241
+ trace: WaveformTrace,
242
+ *,
243
+ z0_source: float = 50.0,
244
+ velocity_factor: float = 0.66,
245
+ threshold: float = 5.0,
246
+ min_separation: float = 1e-12,
247
+ ) -> list[Discontinuity]:
248
+ """Analyze impedance discontinuities in TDR waveform.
249
+
250
+ Detects and characterizes impedance discontinuities along a
251
+ transmission line from TDR measurements.
252
+
253
+ Args:
254
+ trace: TDR reflection waveform.
255
+ z0_source: Source/reference impedance.
256
+ velocity_factor: Fraction of speed of light.
257
+ threshold: Minimum impedance change to detect (ohms).
258
+ min_separation: Minimum time between discontinuities (seconds).
259
+
260
+ Returns:
261
+ List of detected Discontinuity objects.
262
+
263
+ Example:
264
+ >>> disconts = discontinuity_analysis(tdr_trace)
265
+ >>> for d in disconts:
266
+ ... print(f"{d.position*1000:.1f}mm: {d.magnitude:.1f} ohms")
267
+ """
268
+ # Get impedance profile
269
+ _, profile = extract_impedance(
270
+ trace,
271
+ z0_source=z0_source,
272
+ velocity_factor=velocity_factor,
273
+ )
274
+
275
+ impedance = profile.impedance
276
+ time_axis = profile.time
277
+ distance_axis = profile.distance
278
+
279
+ # Find discontinuities using derivative
280
+ derivative = np.abs(np.diff(impedance))
281
+
282
+ # Smooth derivative
283
+ if len(derivative) > 5:
284
+ kernel = np.ones(5) / 5
285
+ derivative = np.convolve(derivative, kernel, mode="same")
286
+
287
+ # Find peaks in derivative (discontinuities)
288
+ sample_rate = trace.metadata.sample_rate
289
+ min_samples = int(min_separation * sample_rate)
290
+
291
+ peaks, _properties = sp_signal.find_peaks(
292
+ derivative,
293
+ height=threshold,
294
+ distance=max(1, min_samples),
295
+ )
296
+
297
+ # Analyze each discontinuity
298
+ discontinuities = []
299
+ for peak_idx in peaks:
300
+ if peak_idx < 1 or peak_idx >= len(impedance) - 1:
301
+ continue
302
+
303
+ z_before = float(np.mean(impedance[max(0, peak_idx - 5) : peak_idx]))
304
+ z_after = float(np.mean(impedance[peak_idx + 1 : min(len(impedance), peak_idx + 6)]))
305
+
306
+ magnitude = z_after - z_before
307
+ position = float(distance_axis[peak_idx])
308
+ time_pos = float(time_axis[peak_idx])
309
+
310
+ # Calculate reflection coefficient
311
+ rho = (z_after - z_before) / (z_after + z_before) if z_before + z_after > 0 else 0.0
312
+
313
+ # Determine discontinuity type
314
+ if magnitude > 0:
315
+ # Increasing impedance
316
+ if abs(magnitude) > 20:
317
+ disc_type: Literal["capacitive", "inductive", "resistive", "unknown"] = "inductive"
318
+ else:
319
+ disc_type = "resistive"
320
+ # Decreasing impedance
321
+ elif abs(magnitude) > 20:
322
+ disc_type = "capacitive"
323
+ else:
324
+ disc_type = "resistive"
325
+
326
+ discontinuities.append(
327
+ Discontinuity(
328
+ position=position,
329
+ time=time_pos,
330
+ impedance_before=z_before,
331
+ impedance_after=z_after,
332
+ magnitude=magnitude,
333
+ reflection_coeff=float(rho),
334
+ discontinuity_type=disc_type,
335
+ )
336
+ )
337
+
338
+ return discontinuities
339
+
340
+
341
+ def _find_incident_level(data: NDArray[np.float64]) -> float:
342
+ """Find the incident step level in TDR data.
343
+
344
+ Looks for the stable level after the initial edge and before
345
+ any reflections return.
346
+
347
+ Args:
348
+ data: TDR waveform data array.
349
+
350
+ Returns:
351
+ Median voltage level in the incident region.
352
+ """
353
+ if len(data) < 10:
354
+ return float(np.max(data))
355
+
356
+ # Look at first 10-20% of data for incident level
357
+ search_end = len(data) // 5
358
+ search_start = len(data) // 20
359
+
360
+ if search_end <= search_start:
361
+ return float(np.max(data[:search_end]))
362
+
363
+ # Find stable region using variance
364
+ stable_data = data[search_start:search_end]
365
+ return float(np.median(stable_data))