oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) 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/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ and integration.
6
6
 
7
7
 
8
8
  Example:
9
- >>> from oscura.math import add, differentiate
9
+ >>> from oscura.utils.math import add, differentiate
10
10
  >>> combined = add(trace1, trace2)
11
11
  >>> derivative = differentiate(trace)
12
12
 
@@ -716,48 +716,21 @@ class _SafeExpressionEvaluator(ast.NodeVisitor):
716
716
  raise AnalysisError(f"AST node type {node.__class__.__name__} not allowed")
717
717
 
718
718
 
719
- def math_expression(
720
- expression: str,
721
- traces: dict[str, WaveformTrace],
722
- *,
723
- channel_name: str | None = None,
724
- ) -> WaveformTrace:
725
- """Evaluate a mathematical expression on traces.
726
-
727
- Evaluates an expression string using named traces as variables.
728
- Supports standard mathematical operations and numpy functions.
719
+ def _validate_trace_compatibility(
720
+ traces: dict[str, WaveformTrace], ref_trace: WaveformTrace
721
+ ) -> None:
722
+ """Validate all traces have same length and sample rate.
729
723
 
730
724
  Args:
731
- expression: Math expression (e.g., "CH1 + CH2", "abs(CH1 - CH2)").
732
- traces: Dictionary mapping variable names to traces.
733
- channel_name: Name for the result trace (optional).
734
-
735
- Returns:
736
- Result WaveformTrace.
725
+ traces: Dictionary of traces to validate.
726
+ ref_trace: Reference trace for comparison.
737
727
 
738
728
  Raises:
739
- AnalysisError: If expression is invalid or traces are incompatible.
740
-
741
- Example:
742
- >>> power = math_expression(
743
- ... "voltage * current",
744
- ... {"voltage": v_trace, "current": i_trace}
745
- ... )
746
-
747
- Security:
748
- Uses AST-based safe evaluation (not eval()). Only whitelisted
749
- operations are permitted: arithmetic, comparisons, and whitelisted
750
- numpy functions. No arbitrary code execution is possible.
729
+ AnalysisError: If traces have incompatible dimensions.
751
730
  """
752
- if not traces:
753
- raise AnalysisError("No traces provided for expression evaluation")
754
-
755
- # Get a reference trace for metadata
756
- ref_trace = next(iter(traces.values()))
731
+ ref_len = len(ref_trace.data)
757
732
  sample_rate = ref_trace.metadata.sample_rate
758
733
 
759
- # Validate all traces have same length and sample rate
760
- ref_len = len(ref_trace.data)
761
734
  for name, trace in traces.items():
762
735
  if len(trace.data) != ref_len:
763
736
  raise AnalysisError(
@@ -771,8 +744,17 @@ def math_expression(
771
744
  details={"expected": sample_rate, "got": trace.metadata.sample_rate}, # type: ignore[arg-type]
772
745
  )
773
746
 
774
- # Create namespace with trace data and safe functions
775
- safe_namespace = {
747
+
748
+ def _build_safe_namespace(traces: dict[str, WaveformTrace]) -> dict[str, Any]:
749
+ """Build safe namespace with trace data and whitelisted functions.
750
+
751
+ Args:
752
+ traces: Dictionary of traces.
753
+
754
+ Returns:
755
+ Namespace dictionary with safe functions and trace data.
756
+ """
757
+ safe_namespace: dict[str, Any] = {
776
758
  "np": np,
777
759
  "abs": np.abs,
778
760
  "sqrt": np.sqrt,
@@ -789,28 +771,67 @@ def math_expression(
789
771
  "pi": np.pi,
790
772
  }
791
773
 
792
- # Add trace data to namespace
793
774
  for name, trace in traces.items():
794
775
  safe_namespace[name] = trace.data.astype(np.float64)
795
776
 
796
- # Use safe AST-based evaluator instead of eval()
797
- evaluator = _SafeExpressionEvaluator(safe_namespace)
777
+ return safe_namespace
778
+
779
+
780
+ def _evaluate_expression(expression: str, namespace: dict[str, Any]) -> Any:
781
+ """Evaluate expression using safe AST-based evaluator.
782
+
783
+ Args:
784
+ expression: Mathematical expression string.
785
+ namespace: Safe namespace with available functions and variables.
786
+
787
+ Returns:
788
+ Evaluated result.
789
+
790
+ Raises:
791
+ AnalysisError: If evaluation fails.
792
+ """
793
+ evaluator = _SafeExpressionEvaluator(namespace)
798
794
  try:
799
- result = evaluator.eval(expression)
795
+ return evaluator.eval(expression)
800
796
  except AnalysisError:
801
- raise # Re-raise AnalysisError from evaluator
797
+ raise
802
798
  except Exception as e:
803
799
  raise AnalysisError(
804
800
  f"Failed to evaluate expression: {e}",
805
801
  details={"expression": expression}, # type: ignore[arg-type]
806
802
  ) from e
807
803
 
804
+
805
+ def _ensure_array_result(result: Any, expected_len: int) -> NDArray[np.float64]:
806
+ """Ensure result is array of expected length.
807
+
808
+ Args:
809
+ result: Evaluation result.
810
+ expected_len: Expected array length.
811
+
812
+ Returns:
813
+ Result as float64 array.
814
+ """
808
815
  if not isinstance(result, np.ndarray):
809
- # Scalar result - broadcast to array
810
- result = np.full(ref_len, result, dtype=np.float64)
816
+ return np.full(expected_len, result, dtype=np.float64)
817
+ return result
811
818
 
812
- new_metadata = TraceMetadata(
813
- sample_rate=sample_rate,
819
+
820
+ def _build_expression_metadata(
821
+ ref_trace: WaveformTrace, expression: str, channel_name: str | None
822
+ ) -> TraceMetadata:
823
+ """Build metadata for expression result trace.
824
+
825
+ Args:
826
+ ref_trace: Reference trace for metadata.
827
+ expression: Expression string (for default naming).
828
+ channel_name: Optional channel name override.
829
+
830
+ Returns:
831
+ Metadata for result trace.
832
+ """
833
+ return TraceMetadata(
834
+ sample_rate=ref_trace.metadata.sample_rate,
814
835
  vertical_scale=None,
815
836
  vertical_offset=None,
816
837
  acquisition_time=ref_trace.metadata.acquisition_time,
@@ -819,4 +840,49 @@ def math_expression(
819
840
  channel_name=channel_name or f"expr({expression[:20]})",
820
841
  )
821
842
 
822
- return WaveformTrace(data=result.astype(np.float64), metadata=new_metadata)
843
+
844
+ def math_expression(
845
+ expression: str,
846
+ traces: dict[str, WaveformTrace],
847
+ *,
848
+ channel_name: str | None = None,
849
+ ) -> WaveformTrace:
850
+ """Evaluate a mathematical expression on traces.
851
+
852
+ Evaluates an expression string using named traces as variables.
853
+ Supports standard mathematical operations and numpy functions.
854
+
855
+ Args:
856
+ expression: Math expression (e.g., "CH1 + CH2", "abs(CH1 - CH2)").
857
+ traces: Dictionary mapping variable names to traces.
858
+ channel_name: Name for the result trace (optional).
859
+
860
+ Returns:
861
+ Result WaveformTrace.
862
+
863
+ Raises:
864
+ AnalysisError: If expression is invalid or traces are incompatible.
865
+
866
+ Example:
867
+ >>> power = math_expression(
868
+ ... "voltage * current",
869
+ ... {"voltage": v_trace, "current": i_trace}
870
+ ... )
871
+
872
+ Security:
873
+ Uses AST-based safe evaluation (not eval()). Only whitelisted
874
+ operations are permitted: arithmetic, comparisons, and whitelisted
875
+ numpy functions. No arbitrary code execution is possible.
876
+ """
877
+ if not traces:
878
+ raise AnalysisError("No traces provided for expression evaluation")
879
+
880
+ ref_trace = next(iter(traces.values()))
881
+ _validate_trace_compatibility(traces, ref_trace)
882
+
883
+ safe_namespace = _build_safe_namespace(traces)
884
+ result = _evaluate_expression(expression, safe_namespace)
885
+ result = _ensure_array_result(result, len(ref_trace.data))
886
+
887
+ metadata = _build_expression_metadata(ref_trace, expression, channel_name)
888
+ return WaveformTrace(data=result.astype(np.float64), metadata=metadata)
@@ -5,7 +5,7 @@ functions for waveform data.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.math import resample, align_traces
8
+ >>> from oscura.utils.math import resample, align_traces
9
9
  >>> resampled = resample(trace, new_sample_rate=1e6)
10
10
  >>> aligned = align_traces(trace1, trace2)
11
11
 
@@ -16,7 +16,7 @@ References:
16
16
  from __future__ import annotations
17
17
 
18
18
  import warnings
19
- from typing import TYPE_CHECKING, Literal
19
+ from typing import TYPE_CHECKING, Any, Literal
20
20
 
21
21
  import numpy as np
22
22
  from scipy import interpolate as sp_interp
@@ -45,13 +45,8 @@ def interpolate(
45
45
  Args:
46
46
  trace: Input trace.
47
47
  new_time: New time points in seconds.
48
- method: Interpolation method:
49
- - "linear": Linear interpolation (default)
50
- - "cubic": Cubic spline interpolation
51
- - "nearest": Nearest neighbor
52
- - "zero": Zero-order hold (step function)
48
+ method: Interpolation method ("linear", "cubic", "nearest", "zero").
53
49
  fill_value: Value for points outside original range.
54
- Can be a single value or (below, above) tuple.
55
50
  channel_name: Name for the result trace (optional).
56
51
 
57
52
  Returns:
@@ -73,55 +68,49 @@ def interpolate(
73
68
  analysis_type="interpolate",
74
69
  )
75
70
 
76
- original_time = trace.time_vector
77
- data = trace.data.astype(np.float64)
71
+ # Create interpolator and interpolate
72
+ interp_func = _create_interpolator(
73
+ trace.time_vector, trace.data.astype(np.float64), method, fill_value
74
+ )
75
+ result_data = interp_func(new_time)
78
76
 
79
- # Create interpolator
80
- if method == "linear":
81
- interp_func = sp_interp.interp1d(
82
- original_time,
83
- data,
84
- kind="linear",
85
- bounds_error=False,
86
- fill_value=fill_value,
87
- )
88
- elif method == "cubic":
89
- interp_func = sp_interp.interp1d(
90
- original_time,
91
- data,
92
- kind="cubic",
93
- bounds_error=False,
94
- fill_value=fill_value,
95
- )
96
- elif method == "nearest":
97
- interp_func = sp_interp.interp1d(
98
- original_time,
99
- data,
100
- kind="nearest",
101
- bounds_error=False,
102
- fill_value=fill_value,
103
- )
104
- elif method == "zero":
105
- interp_func = sp_interp.interp1d(
106
- original_time,
107
- data,
108
- kind="zero",
109
- bounds_error=False,
110
- fill_value=fill_value,
111
- )
112
- else:
77
+ # Build result trace
78
+ new_sample_rate = _calculate_new_sample_rate(new_time, trace.metadata.sample_rate)
79
+ new_metadata = _create_interpolated_metadata(trace, new_sample_rate, channel_name)
80
+
81
+ return WaveformTrace(data=result_data.astype(np.float64), metadata=new_metadata)
82
+
83
+
84
+ def _create_interpolator(
85
+ original_time: NDArray[np.float64],
86
+ data: NDArray[np.float64],
87
+ method: str,
88
+ fill_value: float | tuple[float, float],
89
+ ) -> Any:
90
+ """Create scipy interpolation function."""
91
+ valid_methods = {"linear", "cubic", "nearest", "zero"}
92
+ if method not in valid_methods:
113
93
  raise ValueError(f"Unknown interpolation method: {method}")
114
94
 
115
- # Interpolate
116
- result_data = interp_func(new_time)
95
+ return sp_interp.interp1d(
96
+ original_time, data, kind=method, bounds_error=False, fill_value=fill_value
97
+ )
98
+
117
99
 
118
- # Calculate new sample rate from time points
100
+ def _calculate_new_sample_rate(new_time: NDArray[np.float64], original_sample_rate: float) -> float:
101
+ """Calculate new sample rate from time points."""
119
102
  if len(new_time) > 1:
120
- new_sample_rate = 1.0 / np.mean(np.diff(new_time))
121
- else:
122
- new_sample_rate = trace.metadata.sample_rate
103
+ mean_diff: np.floating[Any] = np.mean(np.diff(new_time))
104
+ new_rate: float = float(1.0 / mean_diff)
105
+ return new_rate
106
+ return original_sample_rate
123
107
 
124
- new_metadata = TraceMetadata(
108
+
109
+ def _create_interpolated_metadata(
110
+ trace: WaveformTrace, new_sample_rate: float, channel_name: str | None
111
+ ) -> TraceMetadata:
112
+ """Create metadata for interpolated trace."""
113
+ return TraceMetadata(
125
114
  sample_rate=new_sample_rate,
126
115
  vertical_scale=trace.metadata.vertical_scale,
127
116
  vertical_offset=trace.metadata.vertical_offset,
@@ -131,7 +120,91 @@ def interpolate(
131
120
  channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_interp",
132
121
  )
133
122
 
134
- return WaveformTrace(data=result_data.astype(np.float64), metadata=new_metadata)
123
+
124
+ def _calculate_target_params(
125
+ new_sample_rate: float | None,
126
+ num_samples: int | None,
127
+ original_rate: float,
128
+ original_samples: int,
129
+ ) -> tuple[float, int]:
130
+ """Calculate target sample rate and sample count for resampling."""
131
+ if new_sample_rate is not None:
132
+ target_rate = new_sample_rate
133
+ target_samples = round(original_samples * target_rate / original_rate)
134
+ else:
135
+ target_samples = num_samples # type: ignore[assignment]
136
+ target_rate = original_rate * target_samples / original_samples
137
+ return target_rate, target_samples
138
+
139
+
140
+ def _check_nyquist_violation(
141
+ data: NDArray[np.float64], original_rate: float, target_rate: float
142
+ ) -> None:
143
+ """Validate Nyquist criterion when downsampling and warn if violated."""
144
+ fft_data = np.fft.rfft(data)
145
+ fft_freqs = np.fft.rfftfreq(len(data), 1 / original_rate)
146
+ power = np.abs(fft_data) ** 2
147
+ power_threshold = 0.01 * np.max(power)
148
+ significant_freqs = fft_freqs[power > power_threshold]
149
+
150
+ if len(significant_freqs) > 0:
151
+ max_frequency = np.max(significant_freqs)
152
+ nyquist_required = 2 * max_frequency
153
+ if target_rate < nyquist_required:
154
+ warnings.warn(
155
+ f"Downsampling to {target_rate:.2e} Hz violates Nyquist criterion. "
156
+ f"Maximum signal frequency is ~{max_frequency:.2e} Hz, "
157
+ f"requiring ≥{nyquist_required:.2e} Hz sample rate. "
158
+ f"Aliasing may occur.",
159
+ UserWarning,
160
+ stacklevel=3,
161
+ )
162
+
163
+
164
+ def _apply_anti_alias_filter(
165
+ data: NDArray[np.float64], target_rate: float, original_rate: float
166
+ ) -> NDArray[np.float64]:
167
+ """Apply lowpass anti-aliasing filter before downsampling."""
168
+ nyquist = target_rate / 2
169
+ cutoff = nyquist / original_rate * 2 # Normalized frequency
170
+ if cutoff < 1.0:
171
+ b, a = sp_signal.butter(8, min(cutoff * 0.9, 0.99), btype="low")
172
+ filtered: NDArray[np.float64] = np.asarray(sp_signal.filtfilt(b, a, data), dtype=np.float64)
173
+ return filtered
174
+ return data
175
+
176
+
177
+ def _perform_resampling(
178
+ data: NDArray[np.float64],
179
+ method: Literal["fft", "polyphase", "interp"],
180
+ target_samples: int,
181
+ original_samples: int,
182
+ original_rate: float,
183
+ target_rate: float,
184
+ ) -> NDArray[np.float64]:
185
+ """Perform the actual resampling based on selected method."""
186
+ if method == "fft":
187
+ resampled: NDArray[np.float64] = np.asarray(
188
+ sp_signal.resample(data, target_samples), dtype=np.float64
189
+ )
190
+ return resampled
191
+ elif method == "polyphase":
192
+ from fractions import Fraction
193
+
194
+ ratio = Fraction(target_samples, original_samples).limit_denominator(1000)
195
+ up, down = ratio.numerator, ratio.denominator
196
+ result = sp_signal.resample_poly(data, up, down)
197
+ truncated: NDArray[np.float64] = np.asarray(result[:target_samples], dtype=np.float64)
198
+ return truncated
199
+ elif method == "interp":
200
+ old_time = np.arange(original_samples) / original_rate
201
+ new_time = np.arange(target_samples) / target_rate
202
+ interpolated: NDArray[np.float64] = np.asarray(
203
+ np.interp(new_time, old_time, data), dtype=np.float64
204
+ )
205
+ return interpolated
206
+ else:
207
+ raise ValueError(f"Unknown resampling method: {method}")
135
208
 
136
209
 
137
210
  def resample(
@@ -175,9 +248,9 @@ def resample(
175
248
  References:
176
249
  MEM-012 (downsampling for memory management)
177
250
  """
251
+ # Validate inputs
178
252
  if (new_sample_rate is None) == (num_samples is None):
179
253
  raise ValueError("Specify exactly one of new_sample_rate or num_samples")
180
-
181
254
  if len(trace.data) < 2:
182
255
  raise InsufficientDataError(
183
256
  "Need at least 2 samples for resampling",
@@ -186,73 +259,33 @@ def resample(
186
259
  analysis_type="resample",
187
260
  )
188
261
 
262
+ # Setup
189
263
  data = trace.data.astype(np.float64)
190
264
  original_rate = trace.metadata.sample_rate
191
265
  original_samples = len(data)
192
266
 
193
267
  # Calculate target parameters
194
- if new_sample_rate is not None:
195
- target_rate = new_sample_rate
196
- target_samples = round(original_samples * target_rate / original_rate)
197
- else:
198
- target_samples = num_samples # type: ignore[assignment]
199
- target_rate = original_rate * target_samples / original_samples
268
+ target_rate, target_samples = _calculate_target_params(
269
+ new_sample_rate, num_samples, original_rate, original_samples
270
+ )
200
271
 
201
272
  if target_samples < 1:
202
273
  raise ValueError("Target number of samples must be at least 1")
203
274
 
204
275
  # REQ: API-019 - Validate Nyquist criterion when downsampling
205
276
  if target_rate < original_rate:
206
- # Estimate maximum frequency using FFT
207
- fft_data = np.fft.rfft(data)
208
- fft_freqs = np.fft.rfftfreq(len(data), 1 / original_rate)
209
- # Find frequency with 90% of max power as max frequency
210
- power = np.abs(fft_data) ** 2
211
- power_threshold = 0.01 * np.max(power) # 1% of max power
212
- significant_freqs = fft_freqs[power > power_threshold]
213
- if len(significant_freqs) > 0:
214
- max_frequency = np.max(significant_freqs)
215
- nyquist_required = 2 * max_frequency
216
- if target_rate < nyquist_required:
217
- warnings.warn(
218
- f"Downsampling to {target_rate:.2e} Hz violates Nyquist criterion. "
219
- f"Maximum signal frequency is ~{max_frequency:.2e} Hz, "
220
- f"requiring ≥{nyquist_required:.2e} Hz sample rate. "
221
- f"Aliasing may occur.",
222
- UserWarning,
223
- stacklevel=2,
224
- )
225
-
226
- # Check if downsampling and apply anti-alias filter
277
+ _check_nyquist_violation(data, original_rate, target_rate)
278
+
279
+ # Apply anti-aliasing filter if downsampling
227
280
  if anti_alias and target_samples < original_samples:
228
- # Lowpass filter at Nyquist of new rate
229
- nyquist = target_rate / 2
230
- cutoff = nyquist / original_rate * 2 # Normalized frequency
231
- if cutoff < 1.0:
232
- # Design lowpass filter
233
- b, a = sp_signal.butter(8, min(cutoff * 0.9, 0.99), btype="low")
234
- data = sp_signal.filtfilt(b, a, data)
235
-
236
- # Resample
237
- if method == "fft":
238
- result_data = sp_signal.resample(data, target_samples)
239
- elif method == "polyphase":
240
- # Find rational approximation for polyphase resampling
241
- from fractions import Fraction
281
+ data = _apply_anti_alias_filter(data, target_rate, original_rate)
242
282
 
243
- ratio = Fraction(target_samples, original_samples).limit_denominator(1000)
244
- up, down = ratio.numerator, ratio.denominator
245
- result_data = sp_signal.resample_poly(data, up, down)
246
- # Trim to exact length
247
- result_data = result_data[:target_samples]
248
- elif method == "interp":
249
- # Simple interpolation
250
- old_time = np.arange(original_samples) / original_rate
251
- new_time = np.arange(target_samples) / target_rate
252
- result_data = np.interp(new_time, old_time, data)
253
- else:
254
- raise ValueError(f"Unknown resampling method: {method}")
283
+ # Perform resampling
284
+ result_data = _perform_resampling(
285
+ data, method, target_samples, original_samples, original_rate, target_rate
286
+ )
255
287
 
288
+ # Build output trace
256
289
  new_metadata = TraceMetadata(
257
290
  sample_rate=target_rate,
258
291
  vertical_scale=trace.metadata.vertical_scale,