oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -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,