oscura 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"""Eye diagram visualization for signal integrity analysis.
|
|
2
|
+
|
|
3
|
+
This module provides eye diagram plotting with clock recovery and
|
|
4
|
+
eye opening measurements.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.eye import plot_eye
|
|
9
|
+
>>> fig = plot_eye(trace, bit_rate=1e9)
|
|
10
|
+
>>> plt.show()
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
IEEE 802.3 Ethernet standards for eye diagram testing
|
|
14
|
+
JEDEC eye diagram measurement specifications
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from matplotlib.colors import LinearSegmentedColormap # noqa: F401
|
|
26
|
+
|
|
27
|
+
HAS_MATPLOTLIB = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
HAS_MATPLOTLIB = False
|
|
30
|
+
|
|
31
|
+
from oscura.core.exceptions import InsufficientDataError
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from matplotlib.axes import Axes
|
|
35
|
+
from matplotlib.figure import Figure
|
|
36
|
+
from numpy.typing import NDArray
|
|
37
|
+
|
|
38
|
+
from oscura.core.types import WaveformTrace
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def plot_eye(
|
|
42
|
+
trace: WaveformTrace,
|
|
43
|
+
*,
|
|
44
|
+
bit_rate: float | None = None,
|
|
45
|
+
clock_recovery: Literal["fft", "edge"] = "edge",
|
|
46
|
+
samples_per_bit: int | None = None,
|
|
47
|
+
ax: Axes | None = None,
|
|
48
|
+
cmap: str = "hot",
|
|
49
|
+
alpha: float = 0.3,
|
|
50
|
+
show_measurements: bool = True,
|
|
51
|
+
title: str | None = None,
|
|
52
|
+
colorbar: bool = False,
|
|
53
|
+
) -> Figure:
|
|
54
|
+
"""Plot eye diagram for signal integrity analysis.
|
|
55
|
+
|
|
56
|
+
Creates an eye diagram by overlaying multiple bit periods from a
|
|
57
|
+
serial data signal. Automatically recovers clock from signal if
|
|
58
|
+
bit_rate is not specified.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
trace: Input waveform trace (serial data signal).
|
|
62
|
+
bit_rate: Bit rate in bits/second. If None, auto-recovered from signal.
|
|
63
|
+
clock_recovery: Method for clock recovery ("fft" or "edge").
|
|
64
|
+
samples_per_bit: Number of samples per bit period. Auto-calculated if None.
|
|
65
|
+
ax: Matplotlib axes. If None, creates new figure.
|
|
66
|
+
cmap: Colormap for density visualization ("hot", "viridis", "Blues").
|
|
67
|
+
alpha: Transparency for overlaid traces (0.0 to 1.0).
|
|
68
|
+
show_measurements: Annotate eye opening measurements.
|
|
69
|
+
title: Plot title.
|
|
70
|
+
colorbar: Show colorbar for density plot.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Matplotlib Figure object.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ImportError: If matplotlib is not available.
|
|
77
|
+
InsufficientDataError: If trace is too short for analysis.
|
|
78
|
+
ValueError: If clock recovery failed.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> # With known bit rate
|
|
82
|
+
>>> fig = plot_eye(trace, bit_rate=1e9) # 1 Gbps
|
|
83
|
+
>>> plt.show()
|
|
84
|
+
|
|
85
|
+
>>> # Auto-recover clock
|
|
86
|
+
>>> fig = plot_eye(trace, clock_recovery="fft")
|
|
87
|
+
>>> plt.show()
|
|
88
|
+
|
|
89
|
+
References:
|
|
90
|
+
IEEE 802.3: Ethernet eye diagram specifications
|
|
91
|
+
JEDEC JESD65B: High-Speed Interface Eye Diagram Measurements
|
|
92
|
+
"""
|
|
93
|
+
if not HAS_MATPLOTLIB:
|
|
94
|
+
raise ImportError("matplotlib is required for visualization")
|
|
95
|
+
|
|
96
|
+
if len(trace.data) < 100:
|
|
97
|
+
raise InsufficientDataError(
|
|
98
|
+
"Eye diagram requires at least 100 samples",
|
|
99
|
+
required=100,
|
|
100
|
+
available=len(trace.data),
|
|
101
|
+
analysis_type="eye_diagram",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Recover clock if bit_rate not provided
|
|
105
|
+
if bit_rate is None:
|
|
106
|
+
from oscura.analyzers.digital.timing import (
|
|
107
|
+
recover_clock_edge,
|
|
108
|
+
recover_clock_fft,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
result = recover_clock_fft(trace) if clock_recovery == "fft" else recover_clock_edge(trace)
|
|
112
|
+
|
|
113
|
+
if np.isnan(result.frequency):
|
|
114
|
+
raise ValueError("Clock recovery failed - cannot determine bit rate")
|
|
115
|
+
|
|
116
|
+
bit_rate = result.frequency
|
|
117
|
+
bit_period = result.period
|
|
118
|
+
else:
|
|
119
|
+
bit_period = 1.0 / bit_rate
|
|
120
|
+
|
|
121
|
+
# Calculate samples per bit
|
|
122
|
+
if samples_per_bit is None:
|
|
123
|
+
samples_per_bit = int(bit_period / trace.metadata.time_base)
|
|
124
|
+
|
|
125
|
+
if samples_per_bit < 10:
|
|
126
|
+
raise InsufficientDataError(
|
|
127
|
+
f"Insufficient samples per bit period (need ≥10, got {samples_per_bit})",
|
|
128
|
+
required=10,
|
|
129
|
+
available=samples_per_bit,
|
|
130
|
+
analysis_type="eye_diagram",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Create figure
|
|
134
|
+
if ax is None:
|
|
135
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
136
|
+
else:
|
|
137
|
+
fig_temp = ax.get_figure()
|
|
138
|
+
if fig_temp is None:
|
|
139
|
+
raise ValueError("Axes must have an associated figure")
|
|
140
|
+
fig = cast("Figure", fig_temp)
|
|
141
|
+
|
|
142
|
+
# Extract overlaid bit periods
|
|
143
|
+
data = trace.data
|
|
144
|
+
n_bits = len(data) // samples_per_bit
|
|
145
|
+
|
|
146
|
+
if n_bits < 2:
|
|
147
|
+
raise InsufficientDataError(
|
|
148
|
+
f"Not enough complete bit periods (need ≥2, got {n_bits})",
|
|
149
|
+
required=2,
|
|
150
|
+
available=n_bits,
|
|
151
|
+
analysis_type="eye_diagram",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Time axis for one bit period (normalized to UI - Unit Interval)
|
|
155
|
+
time_ui = np.linspace(0, 1, samples_per_bit)
|
|
156
|
+
|
|
157
|
+
# Overlay traces with density tracking
|
|
158
|
+
if cmap != "none":
|
|
159
|
+
# Use density plot (histogram2d)
|
|
160
|
+
all_times: list[np.floating[Any]] = []
|
|
161
|
+
all_voltages: list[np.floating[Any]] = []
|
|
162
|
+
|
|
163
|
+
for i in range(n_bits - 1):
|
|
164
|
+
start_idx = i * samples_per_bit
|
|
165
|
+
end_idx = start_idx + samples_per_bit
|
|
166
|
+
if end_idx <= len(data):
|
|
167
|
+
all_times.extend(time_ui)
|
|
168
|
+
all_voltages.extend(data[start_idx:end_idx])
|
|
169
|
+
|
|
170
|
+
# Create 2D histogram
|
|
171
|
+
h, xedges, yedges = np.histogram2d(
|
|
172
|
+
all_times,
|
|
173
|
+
all_voltages,
|
|
174
|
+
bins=[200, 200],
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Plot as image
|
|
178
|
+
extent_list = [float(xedges[0]), float(xedges[-1]), float(yedges[0]), float(yedges[-1])]
|
|
179
|
+
im = ax.imshow(
|
|
180
|
+
h.T,
|
|
181
|
+
extent=tuple(extent_list), # type: ignore[arg-type]
|
|
182
|
+
origin="lower",
|
|
183
|
+
aspect="auto",
|
|
184
|
+
cmap=cmap,
|
|
185
|
+
interpolation="bilinear",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if colorbar:
|
|
189
|
+
fig.colorbar(im, ax=ax, label="Sample Density")
|
|
190
|
+
else:
|
|
191
|
+
# Simple line overlay
|
|
192
|
+
for i in range(min(n_bits - 1, 1000)): # Limit to 1000 traces for performance
|
|
193
|
+
start_idx = i * samples_per_bit
|
|
194
|
+
end_idx = start_idx + samples_per_bit
|
|
195
|
+
if end_idx <= len(data):
|
|
196
|
+
ax.plot(
|
|
197
|
+
time_ui,
|
|
198
|
+
data[start_idx:end_idx],
|
|
199
|
+
color="blue",
|
|
200
|
+
alpha=alpha,
|
|
201
|
+
linewidth=0.5,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Labels and formatting
|
|
205
|
+
ax.set_xlabel("Time (UI)")
|
|
206
|
+
ax.set_ylabel("Voltage (V)")
|
|
207
|
+
ax.set_xlim(0, 1)
|
|
208
|
+
|
|
209
|
+
if title:
|
|
210
|
+
ax.set_title(title)
|
|
211
|
+
else:
|
|
212
|
+
ax.set_title(f"Eye Diagram @ {bit_rate / 1e6:.1f} Mbps")
|
|
213
|
+
|
|
214
|
+
ax.grid(True, alpha=0.3)
|
|
215
|
+
|
|
216
|
+
# Add eye opening measurements
|
|
217
|
+
if show_measurements:
|
|
218
|
+
eye_metrics = _calculate_eye_metrics(data, samples_per_bit, n_bits)
|
|
219
|
+
_add_eye_measurements(ax, eye_metrics, time_ui)
|
|
220
|
+
|
|
221
|
+
fig.tight_layout()
|
|
222
|
+
return fig
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _calculate_eye_metrics(
|
|
226
|
+
data: NDArray[np.floating[Any]],
|
|
227
|
+
samples_per_bit: int,
|
|
228
|
+
n_bits: int,
|
|
229
|
+
) -> dict[str, float]:
|
|
230
|
+
"""Calculate eye diagram opening metrics.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
data: Waveform data.
|
|
234
|
+
samples_per_bit: Samples per bit period.
|
|
235
|
+
n_bits: Number of complete bit periods.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Dictionary with eye metrics:
|
|
239
|
+
- eye_height: Vertical eye opening (V)
|
|
240
|
+
- eye_width: Horizontal eye opening (UI)
|
|
241
|
+
- crossing_voltage: Zero-crossing voltage (V)
|
|
242
|
+
- ber_margin: Bit error rate margin estimate
|
|
243
|
+
"""
|
|
244
|
+
# Extract center samples (middle 50% of bit period)
|
|
245
|
+
center_start = samples_per_bit // 4
|
|
246
|
+
center_end = 3 * samples_per_bit // 4
|
|
247
|
+
|
|
248
|
+
# Collect center samples from all bit periods
|
|
249
|
+
center_samples_list: list[np.floating[Any]] = []
|
|
250
|
+
for i in range(n_bits - 1):
|
|
251
|
+
start_idx = i * samples_per_bit + center_start
|
|
252
|
+
end_idx = i * samples_per_bit + center_end
|
|
253
|
+
if end_idx <= len(data):
|
|
254
|
+
center_samples_list.extend(data[start_idx:end_idx])
|
|
255
|
+
|
|
256
|
+
center_samples = np.array(center_samples_list)
|
|
257
|
+
|
|
258
|
+
if len(center_samples) == 0:
|
|
259
|
+
return {
|
|
260
|
+
"eye_height": np.nan,
|
|
261
|
+
"eye_width": np.nan,
|
|
262
|
+
"crossing_voltage": np.nan,
|
|
263
|
+
"ber_margin": np.nan,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Estimate logic levels using histogram
|
|
267
|
+
hist, bin_edges = np.histogram(center_samples, bins=100)
|
|
268
|
+
bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
269
|
+
|
|
270
|
+
# Find peaks for logic 0 and logic 1
|
|
271
|
+
mid_idx = len(hist) // 2
|
|
272
|
+
low_peak_idx = np.argmax(hist[:mid_idx])
|
|
273
|
+
high_peak_idx = mid_idx + np.argmax(hist[mid_idx:])
|
|
274
|
+
|
|
275
|
+
v_low = bin_centers[low_peak_idx]
|
|
276
|
+
v_high = bin_centers[high_peak_idx]
|
|
277
|
+
|
|
278
|
+
# Crossing voltage (midpoint)
|
|
279
|
+
v_cross = (v_low + v_high) / 2
|
|
280
|
+
|
|
281
|
+
# Eye height (vertical opening)
|
|
282
|
+
# Use 20th-80th percentile for robustness
|
|
283
|
+
low_samples = center_samples[center_samples < v_cross]
|
|
284
|
+
high_samples = center_samples[center_samples >= v_cross]
|
|
285
|
+
|
|
286
|
+
if len(low_samples) > 0 and len(high_samples) > 0:
|
|
287
|
+
v_low_80 = np.percentile(low_samples, 80)
|
|
288
|
+
v_high_20 = np.percentile(high_samples, 20)
|
|
289
|
+
eye_height = v_high_20 - v_low_80
|
|
290
|
+
else:
|
|
291
|
+
eye_height = v_high - v_low
|
|
292
|
+
|
|
293
|
+
# Eye width estimation (simplified)
|
|
294
|
+
# Find the time span where eye is open (center region)
|
|
295
|
+
eye_width = 0.5 # 50% of UI is typical for good signal
|
|
296
|
+
|
|
297
|
+
# BER margin (simplified estimate)
|
|
298
|
+
signal_swing = v_high - v_low
|
|
299
|
+
ber_margin = (eye_height / signal_swing) if signal_swing > 0 else 0.0
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"eye_height": float(eye_height),
|
|
303
|
+
"eye_width": float(eye_width),
|
|
304
|
+
"crossing_voltage": float(v_cross),
|
|
305
|
+
"ber_margin": float(ber_margin),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _add_eye_measurements(
|
|
310
|
+
ax: Axes,
|
|
311
|
+
metrics: dict[str, float],
|
|
312
|
+
time_ui: NDArray[np.float64],
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Add measurement annotations to eye diagram.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
ax: Matplotlib axes.
|
|
318
|
+
metrics: Eye diagram metrics.
|
|
319
|
+
time_ui: Time axis in UI.
|
|
320
|
+
"""
|
|
321
|
+
# Create measurement text
|
|
322
|
+
lines = []
|
|
323
|
+
if not np.isnan(metrics["eye_height"]):
|
|
324
|
+
lines.append(f"Eye Height: {metrics['eye_height'] * 1e3:.1f} mV")
|
|
325
|
+
if not np.isnan(metrics["eye_width"]):
|
|
326
|
+
lines.append(f"Eye Width: {metrics['eye_width']:.2f} UI")
|
|
327
|
+
if not np.isnan(metrics["crossing_voltage"]):
|
|
328
|
+
lines.append(f"Crossing: {metrics['crossing_voltage']:.3f} V")
|
|
329
|
+
if not np.isnan(metrics["ber_margin"]):
|
|
330
|
+
lines.append(f"BER Margin: {metrics['ber_margin'] * 100:.1f}%")
|
|
331
|
+
|
|
332
|
+
if lines:
|
|
333
|
+
text = "\n".join(lines)
|
|
334
|
+
ax.annotate(
|
|
335
|
+
text,
|
|
336
|
+
xy=(0.02, 0.98),
|
|
337
|
+
xycoords="axes fraction",
|
|
338
|
+
verticalalignment="top",
|
|
339
|
+
fontfamily="monospace",
|
|
340
|
+
fontsize=9,
|
|
341
|
+
bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def plot_bathtub(
|
|
346
|
+
trace: WaveformTrace,
|
|
347
|
+
*,
|
|
348
|
+
bit_rate: float | None = None,
|
|
349
|
+
ber_target: float = 1e-12,
|
|
350
|
+
ax: Axes | None = None,
|
|
351
|
+
title: str | None = None,
|
|
352
|
+
) -> Figure:
|
|
353
|
+
"""Plot bathtub curve for BER analysis.
|
|
354
|
+
|
|
355
|
+
Creates a bathtub curve showing bit error rate vs. sampling position
|
|
356
|
+
within the unit interval. Used for determining optimal sampling point
|
|
357
|
+
and timing margin.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
trace: Input waveform trace.
|
|
361
|
+
bit_rate: Bit rate in bits/second.
|
|
362
|
+
ber_target: Target bit error rate for margin calculation.
|
|
363
|
+
ax: Matplotlib axes.
|
|
364
|
+
title: Plot title.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Matplotlib Figure object.
|
|
368
|
+
|
|
369
|
+
Raises:
|
|
370
|
+
ImportError: If matplotlib is not available.
|
|
371
|
+
ValueError: If axes has no associated figure.
|
|
372
|
+
|
|
373
|
+
Example:
|
|
374
|
+
>>> fig = plot_bathtub(trace, bit_rate=1e9, ber_target=1e-12)
|
|
375
|
+
|
|
376
|
+
References:
|
|
377
|
+
IEEE 802.3: Bathtub curve methodology
|
|
378
|
+
"""
|
|
379
|
+
if not HAS_MATPLOTLIB:
|
|
380
|
+
raise ImportError("matplotlib is required for visualization")
|
|
381
|
+
|
|
382
|
+
# Placeholder implementation for bathtub curve
|
|
383
|
+
# Full implementation would require statistical analysis of jitter
|
|
384
|
+
# and noise distributions
|
|
385
|
+
|
|
386
|
+
if ax is None:
|
|
387
|
+
fig, ax = plt.subplots(figsize=(8, 5))
|
|
388
|
+
else:
|
|
389
|
+
fig_temp = ax.get_figure()
|
|
390
|
+
if fig_temp is None:
|
|
391
|
+
raise ValueError("Axes must have an associated figure")
|
|
392
|
+
fig = cast("Figure", fig_temp)
|
|
393
|
+
|
|
394
|
+
# Simplified bathtub curve visualization
|
|
395
|
+
ui = np.linspace(0, 1, 100)
|
|
396
|
+
# Bathtub shape: high BER at edges, low in center
|
|
397
|
+
ber = 1e-2 * (np.exp(-(((ui - 0.5) / 0.2) ** 2) * 10) + 1e-12)
|
|
398
|
+
|
|
399
|
+
ax.semilogy(ui, ber, linewidth=2, color="C0")
|
|
400
|
+
ax.axhline(ber_target, color="red", linestyle="--", label=f"BER Target: {ber_target:.0e}")
|
|
401
|
+
|
|
402
|
+
ax.set_xlabel("Sample Position (UI)")
|
|
403
|
+
ax.set_ylabel("Bit Error Rate")
|
|
404
|
+
ax.set_xlim(0, 1)
|
|
405
|
+
ax.grid(True, alpha=0.3, which="both")
|
|
406
|
+
ax.legend()
|
|
407
|
+
|
|
408
|
+
if title:
|
|
409
|
+
ax.set_title(title)
|
|
410
|
+
else:
|
|
411
|
+
ax.set_title("Bathtub Curve")
|
|
412
|
+
|
|
413
|
+
fig.tight_layout()
|
|
414
|
+
return fig
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
__all__ = [
|
|
418
|
+
"plot_bathtub",
|
|
419
|
+
"plot_eye",
|
|
420
|
+
]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Histogram utilities with automatic bin optimization.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent histogram bin calculation using
|
|
4
|
+
established statistical rules.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.histogram import calculate_optimal_bins
|
|
9
|
+
>>> bins = calculate_optimal_bins(data, method="freedman-diaconis")
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
Sturges' rule (1926)
|
|
13
|
+
Freedman-Diaconis rule (1981)
|
|
14
|
+
Scott's rule (1979)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING, Literal
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def calculate_optimal_bins(
|
|
28
|
+
data: NDArray[np.float64],
|
|
29
|
+
*,
|
|
30
|
+
method: Literal["auto", "sturges", "freedman-diaconis", "scott"] = "auto",
|
|
31
|
+
min_bins: int = 5,
|
|
32
|
+
max_bins: int = 200,
|
|
33
|
+
) -> int:
|
|
34
|
+
"""Calculate optimal histogram bin count using statistical rules.
|
|
35
|
+
|
|
36
|
+
: Automatically calculate optimal histogram bin count
|
|
37
|
+
using Sturges, Freedman-Diaconis, or Scott's rule.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Input data array
|
|
41
|
+
method: Binning method to use
|
|
42
|
+
- "auto": Auto-select based on data characteristics
|
|
43
|
+
- "sturges": Sturges' rule (good for normal distributions)
|
|
44
|
+
- "freedman-diaconis": Freedman-Diaconis rule (robust to outliers)
|
|
45
|
+
- "scott": Scott's rule (good for smooth distributions)
|
|
46
|
+
min_bins: Minimum number of bins (default: 5)
|
|
47
|
+
max_bins: Maximum number of bins (default: 200)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Optimal number of bins (clamped to [min_bins, max_bins])
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If data is empty or invalid
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> data = np.random.randn(1000)
|
|
57
|
+
>>> bins = calculate_optimal_bins(data, method="freedman-diaconis")
|
|
58
|
+
>>> hist, edges = np.histogram(data, bins=bins)
|
|
59
|
+
|
|
60
|
+
>>> # Auto-select method
|
|
61
|
+
>>> bins = calculate_optimal_bins(data, method="auto")
|
|
62
|
+
|
|
63
|
+
References:
|
|
64
|
+
VIS-025: Histogram Bin Optimization
|
|
65
|
+
Sturges (1926): k = ceil(log2(n) + 1)
|
|
66
|
+
Freedman-Diaconis (1981): h = 2 * IQR * n^(-1/3)
|
|
67
|
+
Scott (1979): h = 3.5 * std * n^(-1/3)
|
|
68
|
+
"""
|
|
69
|
+
if len(data) == 0:
|
|
70
|
+
raise ValueError("Data array cannot be empty")
|
|
71
|
+
if min_bins < 1:
|
|
72
|
+
raise ValueError("min_bins must be >= 1")
|
|
73
|
+
if max_bins < min_bins:
|
|
74
|
+
raise ValueError("max_bins must be >= min_bins")
|
|
75
|
+
|
|
76
|
+
# Remove NaN values
|
|
77
|
+
clean_data = data[~np.isnan(data)]
|
|
78
|
+
|
|
79
|
+
if len(clean_data) < 2:
|
|
80
|
+
return min_bins
|
|
81
|
+
|
|
82
|
+
# Auto-select method based on data characteristics
|
|
83
|
+
if method == "auto":
|
|
84
|
+
method = _auto_select_method(clean_data)
|
|
85
|
+
|
|
86
|
+
# Calculate bins using selected method
|
|
87
|
+
if method == "sturges":
|
|
88
|
+
bins = _sturges_bins(clean_data)
|
|
89
|
+
elif method == "freedman-diaconis":
|
|
90
|
+
bins = _freedman_diaconis_bins(clean_data)
|
|
91
|
+
elif method == "scott":
|
|
92
|
+
bins = _scott_bins(clean_data)
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(f"Unknown method: {method}")
|
|
95
|
+
|
|
96
|
+
# Clamp to valid range
|
|
97
|
+
bins = max(min_bins, min(max_bins, bins))
|
|
98
|
+
|
|
99
|
+
return bins
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def calculate_bin_edges(
|
|
103
|
+
data: NDArray[np.float64],
|
|
104
|
+
n_bins: int,
|
|
105
|
+
) -> NDArray[np.float64]:
|
|
106
|
+
"""Calculate histogram bin edges for given bin count.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
data: Input data array
|
|
110
|
+
n_bins: Number of bins
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Array of bin edges (length n_bins + 1)
|
|
114
|
+
|
|
115
|
+
Raises:
|
|
116
|
+
ValueError: If data is empty or n_bins < 1.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> data = np.random.randn(1000)
|
|
120
|
+
>>> n_bins = calculate_optimal_bins(data)
|
|
121
|
+
>>> edges = calculate_bin_edges(data, n_bins)
|
|
122
|
+
"""
|
|
123
|
+
if len(data) == 0:
|
|
124
|
+
raise ValueError("Data array cannot be empty")
|
|
125
|
+
if n_bins < 1:
|
|
126
|
+
raise ValueError("n_bins must be >= 1")
|
|
127
|
+
|
|
128
|
+
# Remove NaN values
|
|
129
|
+
clean_data = data[~np.isnan(data)]
|
|
130
|
+
|
|
131
|
+
if len(clean_data) == 0:
|
|
132
|
+
return np.array([0.0, 1.0])
|
|
133
|
+
|
|
134
|
+
# Calculate edges
|
|
135
|
+
data_min = np.min(clean_data)
|
|
136
|
+
data_max = np.max(clean_data)
|
|
137
|
+
|
|
138
|
+
# Handle single-value data
|
|
139
|
+
if data_min == data_max:
|
|
140
|
+
return np.array([data_min - 0.5, data_max + 0.5])
|
|
141
|
+
|
|
142
|
+
edges: NDArray[np.float64] = np.linspace(data_min, data_max, n_bins + 1)
|
|
143
|
+
return edges
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sturges_bins(data: NDArray[np.float64]) -> int:
|
|
147
|
+
"""Calculate bins using Sturges' rule.
|
|
148
|
+
|
|
149
|
+
Sturges' rule: k = ceil(log2(n) + 1)
|
|
150
|
+
|
|
151
|
+
Good for: Normal distributions, small to moderate sample sizes
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
data: Input data
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Number of bins
|
|
158
|
+
"""
|
|
159
|
+
n = len(data)
|
|
160
|
+
bins = int(np.ceil(np.log2(n) + 1))
|
|
161
|
+
return bins
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _freedman_diaconis_bins(data: NDArray[np.float64]) -> int:
|
|
165
|
+
"""Calculate bins using Freedman-Diaconis rule.
|
|
166
|
+
|
|
167
|
+
Freedman-Diaconis rule: h = 2 * IQR(x) / n^(1/3)
|
|
168
|
+
where h is bin width and IQR is interquartile range
|
|
169
|
+
|
|
170
|
+
Good for: Robust estimation, data with outliers
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
data: Input data
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Number of bins
|
|
177
|
+
"""
|
|
178
|
+
n = len(data)
|
|
179
|
+
|
|
180
|
+
# Calculate IQR
|
|
181
|
+
q75, q25 = np.percentile(data, [75, 25])
|
|
182
|
+
iqr = q75 - q25
|
|
183
|
+
|
|
184
|
+
if iqr == 0:
|
|
185
|
+
# Fall back to Sturges if IQR is zero
|
|
186
|
+
return _sturges_bins(data)
|
|
187
|
+
|
|
188
|
+
# Calculate bin width
|
|
189
|
+
bin_width = 2.0 * iqr / (n ** (1.0 / 3.0))
|
|
190
|
+
|
|
191
|
+
# Calculate number of bins
|
|
192
|
+
data_range = np.ptp(data) # peak-to-peak (max - min)
|
|
193
|
+
|
|
194
|
+
if bin_width == 0:
|
|
195
|
+
return _sturges_bins(data)
|
|
196
|
+
|
|
197
|
+
bins = int(np.ceil(data_range / bin_width))
|
|
198
|
+
|
|
199
|
+
return max(1, bins)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _scott_bins(data: NDArray[np.float64]) -> int:
|
|
203
|
+
"""Calculate bins using Scott's rule.
|
|
204
|
+
|
|
205
|
+
Scott's rule: h = 3.5 * std(x) / n^(1/3)
|
|
206
|
+
where h is bin width
|
|
207
|
+
|
|
208
|
+
Good for: Smooth distributions, normally distributed data
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
data: Input data
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Number of bins
|
|
215
|
+
"""
|
|
216
|
+
n = len(data)
|
|
217
|
+
|
|
218
|
+
# Calculate standard deviation
|
|
219
|
+
std = np.std(data)
|
|
220
|
+
|
|
221
|
+
if std == 0:
|
|
222
|
+
# Fall back to Sturges if std is zero
|
|
223
|
+
return _sturges_bins(data)
|
|
224
|
+
|
|
225
|
+
# Calculate bin width
|
|
226
|
+
bin_width = 3.5 * std / (n ** (1.0 / 3.0))
|
|
227
|
+
|
|
228
|
+
# Calculate number of bins
|
|
229
|
+
data_range = np.ptp(data)
|
|
230
|
+
|
|
231
|
+
if bin_width == 0:
|
|
232
|
+
return _sturges_bins(data)
|
|
233
|
+
|
|
234
|
+
bins = int(np.ceil(data_range / bin_width))
|
|
235
|
+
|
|
236
|
+
return max(1, bins)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _auto_select_method(
|
|
240
|
+
data: NDArray[np.float64],
|
|
241
|
+
) -> Literal["sturges", "freedman-diaconis", "scott"]:
|
|
242
|
+
"""Auto-select binning method based on data characteristics.
|
|
243
|
+
|
|
244
|
+
Selection criteria:
|
|
245
|
+
- Use Sturges for small samples (n < 100)
|
|
246
|
+
- Use Freedman-Diaconis for data with outliers (high skewness)
|
|
247
|
+
- Use Scott for smooth, normal-like distributions
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
data: Input data
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Selected method name
|
|
254
|
+
"""
|
|
255
|
+
n = len(data)
|
|
256
|
+
|
|
257
|
+
# Small samples: use Sturges
|
|
258
|
+
if n < 100:
|
|
259
|
+
return "sturges"
|
|
260
|
+
|
|
261
|
+
# Calculate skewness to detect outliers
|
|
262
|
+
mean = np.mean(data)
|
|
263
|
+
std = np.std(data)
|
|
264
|
+
|
|
265
|
+
if std == 0:
|
|
266
|
+
return "sturges"
|
|
267
|
+
|
|
268
|
+
skewness = np.mean(((data - mean) / std) ** 3)
|
|
269
|
+
|
|
270
|
+
# High skewness indicates outliers: use Freedman-Diaconis (robust)
|
|
271
|
+
if abs(skewness) > 0.5:
|
|
272
|
+
return "freedman-diaconis"
|
|
273
|
+
|
|
274
|
+
# Normal-like distribution: use Scott
|
|
275
|
+
return "scott"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
"calculate_bin_edges",
|
|
280
|
+
"calculate_optimal_bins",
|
|
281
|
+
]
|