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
@@ -21,6 +21,8 @@ from typing import TYPE_CHECKING
21
21
 
22
22
  import numpy as np
23
23
 
24
+ from oscura.utils.geometry import generate_leader_line
25
+
24
26
  if TYPE_CHECKING:
25
27
  from numpy.typing import NDArray
26
28
 
@@ -165,53 +167,19 @@ def layout_stacked_channels(
165
167
  )
166
168
 
167
169
 
168
- def optimize_annotation_placement(
170
+ def _initialize_placed_annotations(
169
171
  annotations: list[Annotation],
170
- *,
171
- display_width: float = 800.0,
172
- display_height: float = 600.0,
173
- max_iterations: int = 100,
174
- repulsion_strength: float = 10.0,
175
- min_spacing: float = 5.0,
176
172
  ) -> list[PlacedAnnotation]:
177
- """Optimize annotation placement with collision avoidance.
178
-
179
- Uses force-directed layout algorithm to separate overlapping labels
180
- with repulsive forces. Generates leader lines when labels must be
181
- displaced from anchor points.
173
+ """Initialize placed annotations at anchor points.
182
174
 
183
175
  Args:
184
176
  annotations: List of annotations to place.
185
- display_width: Display area width in pixels.
186
- display_height: Display area height in pixels.
187
- max_iterations: Maximum iterations for force-directed layout.
188
- repulsion_strength: Strength of repulsive force between overlapping labels.
189
- min_spacing: Minimum spacing between annotations in pixels.
190
177
 
191
178
  Returns:
192
- List of PlacedAnnotation with optimized positions.
193
-
194
- Raises:
195
- ValueError: If annotations list is empty.
196
-
197
- Example:
198
- >>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
199
- >>> placed = optimize_annotation_placement(annots)
200
- >>> print(f"Needs leader: {placed[0].needs_leader}")
201
-
202
- References:
203
- VIS-016: Annotation Placement Intelligence
204
- Force-directed graph layout (Fruchterman-Reingold)
179
+ List of PlacedAnnotation initially at anchor points.
205
180
  """
206
- if len(annotations) == 0:
207
- raise ValueError("annotations list cannot be empty")
208
-
209
- # Convert annotations to display coordinates
210
- # For now, assume data coordinates are normalized to display units
211
181
  placed = []
212
-
213
182
  for annot in annotations:
214
- # Initial placement at anchor point
215
183
  placed.append(
216
184
  PlacedAnnotation(
217
185
  annotation=annot,
@@ -221,84 +189,126 @@ def optimize_annotation_placement(
221
189
  leader_points=None,
222
190
  )
223
191
  )
192
+ return placed
224
193
 
225
- # Apply force-directed layout to resolve overlaps
226
- for _iteration in range(max_iterations):
227
- moved = False
228
-
229
- # Calculate forces between all pairs
230
- for i in range(len(placed)):
231
- fx = 0.0
232
- fy = 0.0
233
-
234
- for j in range(len(placed)):
235
- if i == j:
236
- continue
237
-
238
- # Check for bounding box overlap
239
- dx = placed[j].display_x - placed[i].display_x
240
- dy = placed[j].display_y - placed[i].display_y
241
-
242
- # Bounding box sizes
243
- w1 = placed[i].annotation.bbox_width
244
- h1 = placed[i].annotation.bbox_height
245
- w2 = placed[j].annotation.bbox_width
246
- h2 = placed[j].annotation.bbox_height
247
-
248
- # Minimum separation (sum of half-widths + spacing)
249
- min_dx = (w1 + w2) / 2 + min_spacing
250
- min_dy = (h1 + h2) / 2 + min_spacing
251
-
252
- # Check if overlapping
253
- if abs(dx) < min_dx and abs(dy) < min_dy:
254
- # Calculate repulsive force
255
- distance = np.sqrt(dx**2 + dy**2)
256
- if distance < 1e-6:
257
- # Avoid division by zero
258
- distance = 1e-6
259
- dx = np.random.randn() * 0.1
260
- dy = np.random.randn() * 0.1
261
-
262
- # Repulsion inversely proportional to distance
263
- force = repulsion_strength / distance
264
-
265
- # Apply force in direction away from overlap
266
- fx -= force * dx / distance
267
- fy -= force * dy / distance
268
-
269
- # Apply forces with damping (priority affects inertia)
270
- damping = 0.5
271
- priority_factor = 1.0 - placed[i].annotation.priority
272
-
273
- # Higher priority annotations move less
274
- step_size = damping * priority_factor
275
-
276
- new_x = placed[i].display_x + fx * step_size
277
- new_y = placed[i].display_y + fy * step_size
278
-
279
- # Clamp to display bounds
280
- new_x = np.clip(new_x, 0, display_width)
281
- new_y = np.clip(new_y, 0, display_height)
282
-
283
- # Update if moved significantly
284
- if abs(new_x - placed[i].display_x) > 0.1 or abs(new_y - placed[i].display_y) > 0.1:
285
- placed[i] = PlacedAnnotation(
286
- annotation=placed[i].annotation,
287
- display_x=new_x,
288
- display_y=new_y,
289
- needs_leader=False,
290
- leader_points=None,
194
+
195
+ def _calculate_repulsive_force(
196
+ placed_i: PlacedAnnotation,
197
+ placed_j: PlacedAnnotation,
198
+ min_spacing: float,
199
+ repulsion_strength: float,
200
+ ) -> tuple[float, float]:
201
+ """Calculate repulsive force between two annotations.
202
+
203
+ Args:
204
+ placed_i: First annotation.
205
+ placed_j: Second annotation.
206
+ min_spacing: Minimum spacing in pixels.
207
+ repulsion_strength: Repulsive force strength.
208
+
209
+ Returns:
210
+ Tuple of (fx, fy) force components.
211
+ """
212
+ # Check for bounding box overlap
213
+ dx = placed_j.display_x - placed_i.display_x
214
+ dy = placed_j.display_y - placed_i.display_y
215
+
216
+ # Bounding box sizes
217
+ w1 = placed_i.annotation.bbox_width
218
+ h1 = placed_i.annotation.bbox_height
219
+ w2 = placed_j.annotation.bbox_width
220
+ h2 = placed_j.annotation.bbox_height
221
+
222
+ # Minimum separation (sum of half-widths + spacing)
223
+ min_dx = (w1 + w2) / 2 + min_spacing
224
+ min_dy = (h1 + h2) / 2 + min_spacing
225
+
226
+ # Check if overlapping
227
+ if abs(dx) < min_dx and abs(dy) < min_dy:
228
+ # Calculate repulsive force
229
+ distance = np.sqrt(dx**2 + dy**2)
230
+ if distance < 1e-6:
231
+ # Avoid division by zero
232
+ distance = 1e-6
233
+ dx = np.random.randn() * 0.1
234
+ dy = np.random.randn() * 0.1
235
+
236
+ # Repulsion inversely proportional to distance
237
+ force = repulsion_strength / distance
238
+
239
+ # Return force in direction away from overlap
240
+ return -force * dx / distance, -force * dy / distance
241
+
242
+ return 0.0, 0.0
243
+
244
+
245
+ def _apply_force_iteration(
246
+ placed: list[PlacedAnnotation],
247
+ display_width: float,
248
+ display_height: float,
249
+ min_spacing: float,
250
+ repulsion_strength: float,
251
+ ) -> bool:
252
+ """Apply one iteration of force-directed layout.
253
+
254
+ Args:
255
+ placed: List of placed annotations to update.
256
+ display_width: Display width for clamping.
257
+ display_height: Display height for clamping.
258
+ min_spacing: Minimum spacing in pixels.
259
+ repulsion_strength: Repulsive force strength.
260
+
261
+ Returns:
262
+ True if any annotation moved significantly.
263
+ """
264
+ moved = False
265
+
266
+ for i in range(len(placed)):
267
+ fx = 0.0
268
+ fy = 0.0
269
+
270
+ # Calculate forces from all other annotations
271
+ for j in range(len(placed)):
272
+ if i != j:
273
+ force_x, force_y = _calculate_repulsive_force(
274
+ placed[i], placed[j], min_spacing, repulsion_strength
291
275
  )
292
- moved = True
276
+ fx += force_x
277
+ fy += force_y
293
278
 
294
- # Converged if nothing moved
295
- if not moved:
296
- break
279
+ # Apply forces with damping (priority affects inertia)
280
+ damping = 0.5
281
+ priority_factor = 1.0 - placed[i].annotation.priority
282
+ step_size = damping * priority_factor
297
283
 
298
- # Determine which annotations need leader lines
299
- # (displaced beyond threshold from original position)
300
- leader_threshold = 20.0 # pixels
284
+ new_x = placed[i].display_x + fx * step_size
285
+ new_y = placed[i].display_y + fy * step_size
301
286
 
287
+ # Clamp to display bounds
288
+ new_x = np.clip(new_x, 0, display_width)
289
+ new_y = np.clip(new_y, 0, display_height)
290
+
291
+ # Update if moved significantly
292
+ if abs(new_x - placed[i].display_x) > 0.1 or abs(new_y - placed[i].display_y) > 0.1:
293
+ placed[i] = PlacedAnnotation(
294
+ annotation=placed[i].annotation,
295
+ display_x=new_x,
296
+ display_y=new_y,
297
+ needs_leader=False,
298
+ leader_points=None,
299
+ )
300
+ moved = True
301
+
302
+ return moved
303
+
304
+
305
+ def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float = 20.0) -> None:
306
+ """Add leader lines to annotations displaced from anchor points.
307
+
308
+ Args:
309
+ placed: List of placed annotations to update in-place.
310
+ leader_threshold: Displacement threshold for leader line in pixels.
311
+ """
302
312
  for i, p in enumerate(placed):
303
313
  anchor_x = p.annotation.x
304
314
  anchor_y = p.annotation.y
@@ -307,7 +317,7 @@ def optimize_annotation_placement(
307
317
 
308
318
  if displacement > leader_threshold:
309
319
  # Generate simple orthogonal leader line
310
- leader_points = _generate_leader_line(
320
+ leader_points = generate_leader_line(
311
321
  (anchor_x, anchor_y),
312
322
  (p.display_x, p.display_y),
313
323
  )
@@ -320,40 +330,65 @@ def optimize_annotation_placement(
320
330
  leader_points=leader_points,
321
331
  )
322
332
 
323
- return placed
324
333
 
334
+ def optimize_annotation_placement(
335
+ annotations: list[Annotation],
336
+ *,
337
+ display_width: float = 800.0,
338
+ display_height: float = 600.0,
339
+ max_iterations: int = 100,
340
+ repulsion_strength: float = 10.0,
341
+ min_spacing: float = 5.0,
342
+ ) -> list[PlacedAnnotation]:
343
+ """Optimize annotation placement with collision avoidance.
325
344
 
326
- def _generate_leader_line(
327
- anchor: tuple[float, float],
328
- label: tuple[float, float],
329
- ) -> list[tuple[float, float]]:
330
- """Generate orthogonal leader line from anchor to label.
345
+ Uses force-directed layout algorithm to separate overlapping labels
346
+ with repulsive forces. Generates leader lines when labels must be
347
+ displaced from anchor points.
331
348
 
332
349
  Args:
333
- anchor: Anchor point (x, y)
334
- label: Label position (x, y)
350
+ annotations: List of annotations to place.
351
+ display_width: Display area width in pixels.
352
+ display_height: Display area height in pixels.
353
+ max_iterations: Maximum iterations for force-directed layout.
354
+ repulsion_strength: Strength of repulsive force between overlapping labels.
355
+ min_spacing: Minimum spacing between annotations in pixels.
335
356
 
336
357
  Returns:
337
- List of points for leader line
358
+ List of PlacedAnnotation with optimized positions.
359
+
360
+ Raises:
361
+ ValueError: If annotations list is empty.
362
+
363
+ Example:
364
+ >>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
365
+ >>> placed = optimize_annotation_placement(annots)
366
+ >>> print(f"Needs leader: {placed[0].needs_leader}")
367
+
368
+ References:
369
+ VIS-016: Annotation Placement Intelligence
370
+ Force-directed graph layout (Fruchterman-Reingold)
338
371
  """
339
- ax, ay = anchor
340
- lx, ly = label
372
+ if len(annotations) == 0:
373
+ raise ValueError("annotations list cannot be empty")
341
374
 
342
- # Simple L-shaped leader: anchor -> midpoint -> label
343
- # Choose horizontal-then-vertical or vertical-then-horizontal
344
- # based on which dimension has larger displacement
375
+ # Data preparation - initialize at anchor points
376
+ placed = _initialize_placed_annotations(annotations)
345
377
 
346
- dx = abs(lx - ax)
347
- dy = abs(ly - ay)
378
+ # Force-directed layout iterations
379
+ for _iteration in range(max_iterations):
380
+ moved = _apply_force_iteration(
381
+ placed, display_width, display_height, min_spacing, repulsion_strength
382
+ )
383
+
384
+ # Converged if nothing moved
385
+ if not moved:
386
+ break
348
387
 
349
- if dx > dy:
350
- # Horizontal-first
351
- mid = (lx, ay)
352
- else:
353
- # Vertical-first
354
- mid = (ax, ly)
388
+ # Annotation - add leader lines for displaced annotations
389
+ _add_leader_lines(placed, leader_threshold=20.0)
355
390
 
356
- return [anchor, mid, label]
391
+ return placed
357
392
 
358
393
 
359
394
  __all__ = [
@@ -65,37 +65,64 @@ def calculate_optimal_y_range(
65
65
  References:
66
66
  VIS-013: Auto Y-Axis Range Optimization
67
67
  """
68
+ clean_data = _validate_and_clean_data(data)
69
+ filtered_data = _filter_outliers(clean_data, outlier_threshold)
70
+ _check_clipping(clean_data, filtered_data, clip_warning_threshold)
71
+
72
+ # Calculate data range
73
+ data_min, data_max = float(np.min(filtered_data)), float(np.max(filtered_data))
74
+ margin = _select_smart_margin(len(filtered_data), margin_percent)
75
+
76
+ # Apply range mode
77
+ if symmetric:
78
+ return _symmetric_range(data_min, data_max, margin)
79
+ else:
80
+ return _asymmetric_range(data_min, data_max, margin)
81
+
82
+
83
+ def _validate_and_clean_data(data: NDArray[np.float64]) -> NDArray[np.float64]:
84
+ """Validate data and remove NaN values."""
68
85
  if len(data) == 0:
69
86
  raise ValueError("Data array is empty")
70
87
 
71
- # Remove NaN values
72
88
  clean_data = data[~np.isnan(data)]
73
-
74
89
  if len(clean_data) == 0:
75
90
  raise ValueError("Data contains only NaN values")
76
91
 
77
- # Use percentile-based outlier detection (1st-99th percentile default)
78
- # This corresponds to approximately 3-sigma for normal distributions
79
- lower_percentile = 0.5
80
- upper_percentile = 99.5
92
+ return clean_data
81
93
 
82
- # Calculate robust statistics using percentiles
83
- np.percentile(clean_data, lower_percentile)
84
- np.percentile(clean_data, upper_percentile)
85
94
 
86
- # Filter outliers beyond threshold sigma
87
- median = np.median(clean_data)
88
- mad = np.median(np.abs(clean_data - median))
95
+ def _filter_outliers(data: NDArray[np.float64], outlier_threshold: float) -> NDArray[np.float64]:
96
+ """Filter outliers using robust MAD-based z-scores.
97
+
98
+ Falls back to standard deviation when MAD = 0 (highly concentrated data).
99
+ """
100
+ median = np.median(data)
101
+ mad = np.median(np.abs(data - median))
89
102
  robust_std = 1.4826 * mad # MAD to std conversion
90
103
 
91
104
  if robust_std > 0:
92
- z_scores = np.abs(clean_data - median) / robust_std
93
- inlier_mask = z_scores <= outlier_threshold
94
- filtered_data = clean_data[inlier_mask]
95
- else:
96
- filtered_data = clean_data
97
-
98
- # Detect clipping
105
+ z_scores = np.abs(data - median) / robust_std
106
+ filtered: NDArray[np.float64] = data[z_scores <= outlier_threshold]
107
+ return filtered
108
+
109
+ # Fallback to standard deviation when MAD = 0
110
+ mean = np.mean(data)
111
+ std = np.std(data)
112
+ if std > 0:
113
+ z_scores = np.abs(data - mean) / std
114
+ filtered = data[z_scores <= outlier_threshold]
115
+ return filtered
116
+
117
+ return data
118
+
119
+
120
+ def _check_clipping(
121
+ clean_data: NDArray[np.float64],
122
+ filtered_data: NDArray[np.float64],
123
+ clip_warning_threshold: float,
124
+ ) -> None:
125
+ """Check and warn if too many samples are clipped."""
99
126
  clipped_fraction = 1.0 - (len(filtered_data) / len(clean_data))
100
127
  if clipped_fraction > clip_warning_threshold:
101
128
  import warnings
@@ -107,34 +134,49 @@ def calculate_optimal_y_range(
107
134
  stacklevel=2,
108
135
  )
109
136
 
110
- # Calculate data range
111
- data_min = np.min(filtered_data)
112
- data_max = np.max(filtered_data)
113
- data_range = data_max - data_min
114
137
 
115
- # Smart margin selection based on data density
116
- if len(filtered_data) > 10000:
117
- # Dense data: smaller margin (2%)
118
- margin = 0.02
119
- elif len(filtered_data) < 100:
120
- # Sparse data: larger margin (10%)
121
- margin = 0.10
122
- else:
123
- # Default margin (5%)
124
- margin = margin_percent / 100.0
138
+ def _select_smart_margin(n_samples: int, margin_percent: float) -> float:
139
+ """Select margin based on data density.
125
140
 
126
- # Apply symmetric range mode for bipolar signals
127
- if symmetric:
128
- max_abs = max(abs(data_min), abs(data_max))
129
- margin_value = max_abs * margin
130
- y_min = -(max_abs + margin_value)
131
- y_max = max_abs + margin_value
132
- else:
133
- margin_value = data_range * margin
134
- y_min = data_min - margin_value
135
- y_max = data_max + margin_value
141
+ Only applies smart margin when using default value (5.0%).
142
+ Otherwise respects user's explicit margin_percent.
143
+ """
144
+ # Always respect explicit user values (non-default)
145
+ if margin_percent != 5.0:
146
+ return margin_percent / 100.0
147
+
148
+ # Apply smart margin only for default value
149
+ if n_samples > 10000:
150
+ return 0.02 # Dense data: smaller margin
151
+ elif n_samples < 100:
152
+ return 0.10 # Sparse data: larger margin
153
+ return margin_percent / 100.0
154
+
155
+
156
+ def _symmetric_range(data_min: float, data_max: float, margin: float) -> tuple[float, float]:
157
+ """Calculate symmetric range for bipolar signals."""
158
+ max_abs = max(abs(data_min), abs(data_max))
159
+
160
+ # Handle constant data
161
+ if max_abs == 0:
162
+ return (-0.5, 0.5) # Default range for constant zero
136
163
 
137
- return (float(y_min), float(y_max))
164
+ margin_value = max_abs * margin
165
+ return (-(max_abs + margin_value), max_abs + margin_value)
166
+
167
+
168
+ def _asymmetric_range(data_min: float, data_max: float, margin: float) -> tuple[float, float]:
169
+ """Calculate asymmetric range."""
170
+ data_range = data_max - data_min
171
+
172
+ # Handle constant data (range = 0)
173
+ if data_range == 0:
174
+ # Add fixed margin for constant data
175
+ default_margin = 0.5 if data_min == 0 else abs(data_min) * 0.1
176
+ return (data_min - default_margin, data_max + default_margin)
177
+
178
+ margin_value = data_range * margin
179
+ return (data_min - margin_value, data_max + margin_value)
138
180
 
139
181
 
140
182
  def calculate_optimal_x_window(
@@ -188,8 +230,10 @@ def calculate_optimal_x_window(
188
230
  active_regions = rms > rms_threshold
189
231
 
190
232
  if not np.any(active_regions):
191
- # No significant activity, return full range
192
- return (float(time[0]), float(time[-1]))
233
+ # No significant activity, return padded full range
234
+ time_range = time[-1] - time[0]
235
+ padding = time_range * 0.05 # 5% padding on each side
236
+ return (float(time[0] - padding), float(time[-1] + padding))
193
237
 
194
238
  # Find first active region
195
239
  active_indices = np.where(active_regions)[0]
@@ -215,21 +259,29 @@ def calculate_optimal_x_window(
215
259
 
216
260
  if len(crossings) >= 4:
217
261
  # Estimate period from crossings (two crossings per cycle)
262
+ # crossings[::2] already gives full periods (every other crossing)
218
263
  periods = np.diff(crossings[::2])
219
264
  if len(periods) > 0:
220
265
  median_period = np.median(periods)
221
- samples_per_feature = int(median_period * 2) # Full cycle
266
+ samples_per_feature = int(median_period) # Already full cycle from [::2]
222
267
 
223
268
  # Calculate window to show target_features
224
269
  total_samples = samples_per_feature * target_features
270
+
271
+ # Respect decimation constraint
272
+ max_window_samples = int(screen_width * samples_per_pixel)
273
+ total_samples = min(total_samples, max_window_samples)
274
+
225
275
  window_start = first_active
226
276
  window_end = min(window_start + total_samples, len(time) - 1)
227
277
 
228
278
  return (float(time[window_start]), float(time[window_end]))
229
279
 
230
- # Fallback: zoom to first N% of active region
280
+ # Fallback: zoom to respect decimation threshold
281
+ # Limit window to screen_width * samples_per_pixel samples
282
+ max_window_samples = int(screen_width * samples_per_pixel)
231
283
  active_duration = len(active_indices)
232
- zoom_samples = min(active_duration, screen_width * int(samples_per_pixel))
284
+ zoom_samples = min(active_duration, max_window_samples)
233
285
  window_end = min(first_active + zoom_samples, len(time) - 1)
234
286
 
235
287
  return (float(time[first_active]), float(time[window_end]))
@@ -650,7 +702,7 @@ def detect_interesting_regions(
650
702
  edge_threshold: float | None = None,
651
703
  glitch_sigma: float = 3.0,
652
704
  anomaly_threshold: float = 3.0,
653
- min_region_samples: int = 10,
705
+ min_region_samples: int = 1,
654
706
  max_regions: int = 10,
655
707
  ) -> list[InterestingRegion]:
656
708
  """Detect interesting regions in a signal for automatic zoom/focus.
@@ -664,7 +716,7 @@ def detect_interesting_regions(
664
716
  edge_threshold: Edge detection threshold (default: auto from signal stddev)
665
717
  glitch_sigma: Sigma threshold for glitch detection (default: 3.0)
666
718
  anomaly_threshold: Threshold for anomaly detection in sigma (default: 3.0)
667
- min_region_samples: Minimum samples per region (default: 10)
719
+ min_region_samples: Minimum samples per region (default: 1)
668
720
  max_regions: Maximum number of regions to return (default: 10)
669
721
 
670
722
  Returns:
@@ -691,7 +743,6 @@ def detect_interesting_regions(
691
743
  raise ValueError("min_region_samples must be >= 1")
692
744
 
693
745
  regions: list[InterestingRegion] = []
694
- 1.0 / sample_rate
695
746
 
696
747
  # 1. Edge detection using first derivative
697
748
  edges = _detect_edges(signal, sample_rate, edge_threshold)
@@ -303,7 +303,7 @@ def show_palette(
303
303
  ax.set_xticklabels(["0", "0.25", "0.5", "0.75", "1.0"])
304
304
  ax.set_title(f"Colormap: {name}")
305
305
  except ValueError:
306
- raise ValueError(f"Unknown palette or colormap: {name}") # noqa: B904
306
+ raise ValueError(f"Unknown palette or colormap: {name}")
307
307
 
308
308
  plt.tight_layout()
309
309