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,381 @@
|
|
|
1
|
+
"""Plot style presets for different output contexts.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive style presets for publication-quality,
|
|
4
|
+
presentation, screen viewing, and print output.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.styles import apply_style_preset
|
|
9
|
+
>>> with apply_style_preset("publication"):
|
|
10
|
+
... plot_waveform(signal)
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
matplotlib rcParams customization
|
|
14
|
+
Publication and presentation best practices
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from contextlib import contextmanager
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Iterator
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import matplotlib.pyplot as plt
|
|
28
|
+
|
|
29
|
+
HAS_MATPLOTLIB = True
|
|
30
|
+
except ImportError:
|
|
31
|
+
HAS_MATPLOTLIB = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class StylePreset:
|
|
36
|
+
"""Style preset configuration for plots.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
name: Preset name
|
|
40
|
+
dpi: Target DPI (dots per inch)
|
|
41
|
+
font_family: Font family (serif, sans-serif, monospace)
|
|
42
|
+
font_size: Base font size in points
|
|
43
|
+
line_width: Default line width in points
|
|
44
|
+
marker_size: Default marker size
|
|
45
|
+
figure_facecolor: Figure background color
|
|
46
|
+
axes_facecolor: Axes background color
|
|
47
|
+
axes_edgecolor: Axes edge color
|
|
48
|
+
grid_color: Grid line color
|
|
49
|
+
grid_alpha: Grid line transparency
|
|
50
|
+
grid_linestyle: Grid line style
|
|
51
|
+
use_latex: Use LaTeX for text rendering
|
|
52
|
+
tight_layout: Use tight layout
|
|
53
|
+
rcparams: Additional matplotlib rcParams
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
dpi: int = 96
|
|
58
|
+
font_family: str = "sans-serif"
|
|
59
|
+
font_size: int = 10
|
|
60
|
+
line_width: float = 1.0
|
|
61
|
+
marker_size: float = 6.0
|
|
62
|
+
figure_facecolor: str = "white"
|
|
63
|
+
axes_facecolor: str = "white"
|
|
64
|
+
axes_edgecolor: str = "black"
|
|
65
|
+
grid_color: str = "#B0B0B0"
|
|
66
|
+
grid_alpha: float = 0.3
|
|
67
|
+
grid_linestyle: str = "-"
|
|
68
|
+
use_latex: bool = False
|
|
69
|
+
tight_layout: bool = True
|
|
70
|
+
rcparams: dict[str, Any] = field(default_factory=dict)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Predefined style presets
|
|
74
|
+
|
|
75
|
+
PUBLICATION_PRESET = StylePreset(
|
|
76
|
+
name="publication",
|
|
77
|
+
dpi=600,
|
|
78
|
+
font_family="serif",
|
|
79
|
+
font_size=10,
|
|
80
|
+
line_width=0.8,
|
|
81
|
+
marker_size=4.0,
|
|
82
|
+
figure_facecolor="white",
|
|
83
|
+
axes_facecolor="white",
|
|
84
|
+
axes_edgecolor="black",
|
|
85
|
+
grid_color="#808080",
|
|
86
|
+
grid_alpha=0.3,
|
|
87
|
+
grid_linestyle=":",
|
|
88
|
+
use_latex=False, # LaTeX optional - requires system install
|
|
89
|
+
tight_layout=True,
|
|
90
|
+
rcparams={
|
|
91
|
+
"axes.linewidth": 0.8,
|
|
92
|
+
"xtick.major.width": 0.8,
|
|
93
|
+
"ytick.major.width": 0.8,
|
|
94
|
+
"xtick.minor.width": 0.6,
|
|
95
|
+
"ytick.minor.width": 0.6,
|
|
96
|
+
"lines.antialiased": True,
|
|
97
|
+
"patch.antialiased": True,
|
|
98
|
+
"savefig.dpi": 600,
|
|
99
|
+
"savefig.format": "pdf",
|
|
100
|
+
"savefig.bbox": "tight",
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
PRESENTATION_PRESET = StylePreset(
|
|
105
|
+
name="presentation",
|
|
106
|
+
dpi=96,
|
|
107
|
+
font_family="sans-serif",
|
|
108
|
+
font_size=18,
|
|
109
|
+
line_width=2.5,
|
|
110
|
+
marker_size=10.0,
|
|
111
|
+
figure_facecolor="white",
|
|
112
|
+
axes_facecolor="white",
|
|
113
|
+
axes_edgecolor="black",
|
|
114
|
+
grid_color="#CCCCCC",
|
|
115
|
+
grid_alpha=0.5,
|
|
116
|
+
grid_linestyle="-",
|
|
117
|
+
use_latex=False,
|
|
118
|
+
tight_layout=True,
|
|
119
|
+
rcparams={
|
|
120
|
+
"axes.linewidth": 2.0,
|
|
121
|
+
"xtick.major.width": 2.0,
|
|
122
|
+
"ytick.major.width": 2.0,
|
|
123
|
+
"xtick.major.size": 8,
|
|
124
|
+
"ytick.major.size": 8,
|
|
125
|
+
"lines.antialiased": True,
|
|
126
|
+
"savefig.dpi": 150,
|
|
127
|
+
},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
SCREEN_PRESET = StylePreset(
|
|
131
|
+
name="screen",
|
|
132
|
+
dpi=96,
|
|
133
|
+
font_family="sans-serif",
|
|
134
|
+
font_size=10,
|
|
135
|
+
line_width=1.2,
|
|
136
|
+
marker_size=6.0,
|
|
137
|
+
figure_facecolor="white",
|
|
138
|
+
axes_facecolor="white",
|
|
139
|
+
axes_edgecolor="#333333",
|
|
140
|
+
grid_color="#B0B0B0",
|
|
141
|
+
grid_alpha=0.3,
|
|
142
|
+
grid_linestyle="-",
|
|
143
|
+
use_latex=False,
|
|
144
|
+
tight_layout=True,
|
|
145
|
+
rcparams={
|
|
146
|
+
"axes.linewidth": 1.0,
|
|
147
|
+
"lines.antialiased": True,
|
|
148
|
+
"patch.antialiased": True,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
PRINT_PRESET = StylePreset(
|
|
153
|
+
name="print",
|
|
154
|
+
dpi=300,
|
|
155
|
+
font_family="serif",
|
|
156
|
+
font_size=11,
|
|
157
|
+
line_width=1.2,
|
|
158
|
+
marker_size=5.0,
|
|
159
|
+
figure_facecolor="white",
|
|
160
|
+
axes_facecolor="white",
|
|
161
|
+
axes_edgecolor="black",
|
|
162
|
+
grid_color="#707070",
|
|
163
|
+
grid_alpha=0.3,
|
|
164
|
+
grid_linestyle=":",
|
|
165
|
+
use_latex=False,
|
|
166
|
+
tight_layout=True,
|
|
167
|
+
rcparams={
|
|
168
|
+
"axes.linewidth": 1.0,
|
|
169
|
+
"xtick.major.width": 1.0,
|
|
170
|
+
"ytick.major.width": 1.0,
|
|
171
|
+
"lines.antialiased": False, # Sharper lines for print
|
|
172
|
+
"patch.antialiased": False,
|
|
173
|
+
"savefig.dpi": 300,
|
|
174
|
+
"savefig.format": "pdf",
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Registry of available presets
|
|
179
|
+
PRESETS: dict[str, StylePreset] = {
|
|
180
|
+
"publication": PUBLICATION_PRESET,
|
|
181
|
+
"presentation": PRESENTATION_PRESET,
|
|
182
|
+
"screen": SCREEN_PRESET,
|
|
183
|
+
"print": PRINT_PRESET,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@contextmanager
|
|
188
|
+
def apply_style_preset(
|
|
189
|
+
preset: str | StylePreset,
|
|
190
|
+
*,
|
|
191
|
+
overrides: dict[str, Any] | None = None,
|
|
192
|
+
) -> Iterator[None]:
|
|
193
|
+
"""Apply style preset as context manager.
|
|
194
|
+
|
|
195
|
+
: Provide comprehensive style presets for common use cases
|
|
196
|
+
with support for custom overrides.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
preset: Preset name or StylePreset object
|
|
200
|
+
overrides: Dictionary of rcParams to override
|
|
201
|
+
|
|
202
|
+
Yields:
|
|
203
|
+
None (use as context manager)
|
|
204
|
+
|
|
205
|
+
Raises:
|
|
206
|
+
ValueError: If preset name is unknown
|
|
207
|
+
ImportError: If matplotlib is not available
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> with apply_style_preset("publication"):
|
|
211
|
+
... fig, ax = plt.subplots()
|
|
212
|
+
... ax.plot(x, y)
|
|
213
|
+
... plt.savefig("figure.pdf")
|
|
214
|
+
|
|
215
|
+
>>> # With overrides
|
|
216
|
+
>>> with apply_style_preset("screen", overrides={"font.size": 14}):
|
|
217
|
+
... plot_waveform(signal)
|
|
218
|
+
|
|
219
|
+
References:
|
|
220
|
+
VIS-024: Plot Style Presets
|
|
221
|
+
matplotlib style sheets and rcParams
|
|
222
|
+
"""
|
|
223
|
+
if not HAS_MATPLOTLIB:
|
|
224
|
+
raise ImportError("matplotlib is required for style presets")
|
|
225
|
+
|
|
226
|
+
# Get preset object
|
|
227
|
+
if isinstance(preset, str):
|
|
228
|
+
if preset not in PRESETS:
|
|
229
|
+
raise ValueError(f"Unknown preset: {preset}. Available: {list(PRESETS.keys())}")
|
|
230
|
+
preset_obj = PRESETS[preset]
|
|
231
|
+
else:
|
|
232
|
+
preset_obj = preset
|
|
233
|
+
|
|
234
|
+
# Build rcParams dictionary
|
|
235
|
+
rc_dict = _preset_to_rcparams(preset_obj)
|
|
236
|
+
|
|
237
|
+
# Apply overrides
|
|
238
|
+
if overrides:
|
|
239
|
+
rc_dict.update(overrides)
|
|
240
|
+
|
|
241
|
+
# Apply as context
|
|
242
|
+
with plt.rc_context(rc_dict):
|
|
243
|
+
yield
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _preset_to_rcparams(preset: StylePreset) -> dict[str, Any]:
|
|
247
|
+
"""Convert StylePreset to matplotlib rcParams dictionary.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
preset: StylePreset object
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Dictionary of rcParams
|
|
254
|
+
"""
|
|
255
|
+
rc = {
|
|
256
|
+
"figure.dpi": preset.dpi,
|
|
257
|
+
"font.family": preset.font_family,
|
|
258
|
+
"font.size": preset.font_size,
|
|
259
|
+
"lines.linewidth": preset.line_width,
|
|
260
|
+
"lines.markersize": preset.marker_size,
|
|
261
|
+
"figure.facecolor": preset.figure_facecolor,
|
|
262
|
+
"axes.facecolor": preset.axes_facecolor,
|
|
263
|
+
"axes.edgecolor": preset.axes_edgecolor,
|
|
264
|
+
"grid.color": preset.grid_color,
|
|
265
|
+
"grid.alpha": preset.grid_alpha,
|
|
266
|
+
"grid.linestyle": preset.grid_linestyle,
|
|
267
|
+
"figure.autolayout": preset.tight_layout,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# LaTeX rendering
|
|
271
|
+
if preset.use_latex:
|
|
272
|
+
rc["text.usetex"] = True
|
|
273
|
+
|
|
274
|
+
# Merge with additional rcparams
|
|
275
|
+
rc.update(preset.rcparams)
|
|
276
|
+
|
|
277
|
+
return rc
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def create_custom_preset(
|
|
281
|
+
name: str,
|
|
282
|
+
base_preset: str = "screen",
|
|
283
|
+
**kwargs: Any,
|
|
284
|
+
) -> StylePreset:
|
|
285
|
+
"""Create custom preset by inheriting from base preset.
|
|
286
|
+
|
|
287
|
+
: Support custom presets with inheritance and override.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
name: Name for custom preset
|
|
291
|
+
base_preset: Base preset to inherit from
|
|
292
|
+
**kwargs: Attributes to override
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Custom StylePreset object
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ValueError: If base_preset is unknown
|
|
299
|
+
|
|
300
|
+
Example:
|
|
301
|
+
>>> custom = create_custom_preset(
|
|
302
|
+
... "my_style",
|
|
303
|
+
... base_preset="publication",
|
|
304
|
+
... font_size=12,
|
|
305
|
+
... line_width=1.5
|
|
306
|
+
... )
|
|
307
|
+
>>> with apply_style_preset(custom):
|
|
308
|
+
... plot_data()
|
|
309
|
+
|
|
310
|
+
References:
|
|
311
|
+
VIS-024: Plot Style Presets with inheritance
|
|
312
|
+
"""
|
|
313
|
+
if base_preset not in PRESETS:
|
|
314
|
+
raise ValueError(f"Unknown base_preset: {base_preset}")
|
|
315
|
+
|
|
316
|
+
# Get base preset
|
|
317
|
+
base = PRESETS[base_preset]
|
|
318
|
+
|
|
319
|
+
# Create copy with overrides
|
|
320
|
+
preset_dict = {
|
|
321
|
+
"name": name,
|
|
322
|
+
"dpi": kwargs.get("dpi", base.dpi),
|
|
323
|
+
"font_family": kwargs.get("font_family", base.font_family),
|
|
324
|
+
"font_size": kwargs.get("font_size", base.font_size),
|
|
325
|
+
"line_width": kwargs.get("line_width", base.line_width),
|
|
326
|
+
"marker_size": kwargs.get("marker_size", base.marker_size),
|
|
327
|
+
"figure_facecolor": kwargs.get("figure_facecolor", base.figure_facecolor),
|
|
328
|
+
"axes_facecolor": kwargs.get("axes_facecolor", base.axes_facecolor),
|
|
329
|
+
"axes_edgecolor": kwargs.get("axes_edgecolor", base.axes_edgecolor),
|
|
330
|
+
"grid_color": kwargs.get("grid_color", base.grid_color),
|
|
331
|
+
"grid_alpha": kwargs.get("grid_alpha", base.grid_alpha),
|
|
332
|
+
"grid_linestyle": kwargs.get("grid_linestyle", base.grid_linestyle),
|
|
333
|
+
"use_latex": kwargs.get("use_latex", base.use_latex),
|
|
334
|
+
"tight_layout": kwargs.get("tight_layout", base.tight_layout),
|
|
335
|
+
"rcparams": kwargs.get("rcparams", base.rcparams.copy()),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return StylePreset(**preset_dict)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def register_preset(preset: StylePreset) -> None:
|
|
342
|
+
"""Register custom preset in global registry.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
preset: StylePreset to register
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
>>> custom = create_custom_preset("my_style", base_preset="publication")
|
|
349
|
+
>>> register_preset(custom)
|
|
350
|
+
>>> with apply_style_preset("my_style"):
|
|
351
|
+
... plot_data()
|
|
352
|
+
"""
|
|
353
|
+
PRESETS[preset.name] = preset
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def list_presets() -> list[str]:
|
|
357
|
+
"""Get list of available preset names.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of preset names
|
|
361
|
+
|
|
362
|
+
Example:
|
|
363
|
+
>>> presets = list_presets()
|
|
364
|
+
>>> print(presets)
|
|
365
|
+
['publication', 'presentation', 'screen', 'print']
|
|
366
|
+
"""
|
|
367
|
+
return list(PRESETS.keys())
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
__all__ = [
|
|
371
|
+
"PRESENTATION_PRESET",
|
|
372
|
+
"PRESETS",
|
|
373
|
+
"PRINT_PRESET",
|
|
374
|
+
"PUBLICATION_PRESET",
|
|
375
|
+
"SCREEN_PRESET",
|
|
376
|
+
"StylePreset",
|
|
377
|
+
"apply_style_preset",
|
|
378
|
+
"create_custom_preset",
|
|
379
|
+
"list_presets",
|
|
380
|
+
"register_preset",
|
|
381
|
+
]
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Thumbnail rendering for fast signal previews.
|
|
2
|
+
|
|
3
|
+
This module provides fast preview rendering with reduced detail
|
|
4
|
+
for gallery and browser contexts.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.thumbnails import render_thumbnail
|
|
9
|
+
>>> fig = render_thumbnail(signal, sample_rate, size=(400, 300))
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
Aggressive decimation for performance
|
|
13
|
+
Simplified rendering without expensive features
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from matplotlib.figure import Figure
|
|
24
|
+
from numpy.typing import NDArray
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import matplotlib # noqa: F401
|
|
28
|
+
import matplotlib.pyplot as plt
|
|
29
|
+
|
|
30
|
+
HAS_MATPLOTLIB = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_MATPLOTLIB = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render_thumbnail(
|
|
36
|
+
signal: NDArray[np.float64],
|
|
37
|
+
sample_rate: float | None = None,
|
|
38
|
+
*,
|
|
39
|
+
size: tuple[int, int] = (400, 300),
|
|
40
|
+
width: int | None = None,
|
|
41
|
+
height: int | None = None,
|
|
42
|
+
max_samples: int = 1000,
|
|
43
|
+
time_unit: str = "auto",
|
|
44
|
+
title: str | None = None,
|
|
45
|
+
dpi: int = 72,
|
|
46
|
+
) -> Figure:
|
|
47
|
+
"""Render fast preview thumbnail of signal.
|
|
48
|
+
|
|
49
|
+
: Fast preview rendering mode with reduced detail,
|
|
50
|
+
simplified styles, and lower resolution for quick plot generation.
|
|
51
|
+
|
|
52
|
+
Target performance: <100ms for typical signals (goal: 50ms)
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
signal: Input signal array
|
|
56
|
+
sample_rate: Sample rate in Hz. If None, uses 1.0 (sample indices as x-axis).
|
|
57
|
+
size: Thumbnail size in pixels (width, height), default (400, 300)
|
|
58
|
+
width: Width in pixels (alternative to size). If specified, height defaults to 3/4 of width.
|
|
59
|
+
height: Height in pixels (alternative to size).
|
|
60
|
+
max_samples: Maximum samples to plot (default: 1000, aggressive decimation)
|
|
61
|
+
time_unit: Time unit for x-axis ("s", "ms", "us", "ns", "auto")
|
|
62
|
+
title: Optional title
|
|
63
|
+
dpi: DPI for rendering (default: 72)
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Matplotlib Figure object configured for fast rendering
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If signal is empty or sample_rate is invalid
|
|
70
|
+
ImportError: If matplotlib is not available
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> signal = np.sin(2*np.pi*1000*np.arange(0, 0.01, 1/1e6))
|
|
74
|
+
>>> fig = render_thumbnail(signal, 1e6, size=(400, 300))
|
|
75
|
+
>>> fig.savefig("preview.png")
|
|
76
|
+
>>> # Without sample rate
|
|
77
|
+
>>> fig = render_thumbnail(data, width=100, height=50)
|
|
78
|
+
|
|
79
|
+
References:
|
|
80
|
+
VIS-018: Thumbnail Mode
|
|
81
|
+
Fixed-count decimation for uniform sampling
|
|
82
|
+
"""
|
|
83
|
+
if not HAS_MATPLOTLIB:
|
|
84
|
+
raise ImportError("matplotlib is required for visualization")
|
|
85
|
+
|
|
86
|
+
# Default sample rate if not provided
|
|
87
|
+
if sample_rate is None:
|
|
88
|
+
sample_rate = 1.0
|
|
89
|
+
|
|
90
|
+
if len(signal) == 0:
|
|
91
|
+
raise ValueError("Signal cannot be empty")
|
|
92
|
+
if sample_rate <= 0:
|
|
93
|
+
raise ValueError("Sample rate must be positive")
|
|
94
|
+
if max_samples < 10:
|
|
95
|
+
raise ValueError("max_samples must be >= 10")
|
|
96
|
+
|
|
97
|
+
# Handle width/height as alternative to size
|
|
98
|
+
if width is not None:
|
|
99
|
+
h = height if height is not None else int(width * 0.75)
|
|
100
|
+
size = (width, h)
|
|
101
|
+
elif height is not None:
|
|
102
|
+
size = (int(height * 4 / 3), height)
|
|
103
|
+
|
|
104
|
+
# Configure matplotlib for fast rendering (no anti-aliasing, etc.)
|
|
105
|
+
with plt.rc_context(
|
|
106
|
+
{
|
|
107
|
+
"path.simplify": True,
|
|
108
|
+
"path.simplify_threshold": 1.0,
|
|
109
|
+
"agg.path.chunksize": 1000,
|
|
110
|
+
"lines.antialiased": False,
|
|
111
|
+
"patch.antialiased": False,
|
|
112
|
+
"text.antialiased": False,
|
|
113
|
+
}
|
|
114
|
+
):
|
|
115
|
+
# Calculate figure size in inches
|
|
116
|
+
width_inches = size[0] / dpi
|
|
117
|
+
height_inches = size[1] / dpi
|
|
118
|
+
|
|
119
|
+
# Create figure with no fancy features
|
|
120
|
+
fig, ax = plt.subplots(figsize=(width_inches, height_inches), dpi=dpi)
|
|
121
|
+
|
|
122
|
+
# Decimate signal to max_samples
|
|
123
|
+
decimated_signal = _decimate_uniform(signal, max_samples)
|
|
124
|
+
|
|
125
|
+
# Create time vector for decimated signal
|
|
126
|
+
total_time = len(signal) / sample_rate
|
|
127
|
+
time = np.linspace(0, total_time, len(decimated_signal))
|
|
128
|
+
|
|
129
|
+
# Auto-select time unit
|
|
130
|
+
if time_unit == "auto":
|
|
131
|
+
if total_time < 1e-6:
|
|
132
|
+
time_unit = "ns"
|
|
133
|
+
elif total_time < 1e-3:
|
|
134
|
+
time_unit = "us"
|
|
135
|
+
elif total_time < 1:
|
|
136
|
+
time_unit = "ms"
|
|
137
|
+
else:
|
|
138
|
+
time_unit = "s"
|
|
139
|
+
|
|
140
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
141
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
142
|
+
time_scaled = time * multiplier
|
|
143
|
+
|
|
144
|
+
# Plot with simplified style
|
|
145
|
+
ax.plot(time_scaled, decimated_signal, "b-", linewidth=0.5, antialiased=False)
|
|
146
|
+
|
|
147
|
+
# Minimal labels (no grid, no fancy formatting)
|
|
148
|
+
ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
|
|
149
|
+
ax.set_ylabel("Amplitude", fontsize=8)
|
|
150
|
+
|
|
151
|
+
if title:
|
|
152
|
+
ax.set_title(title, fontsize=9)
|
|
153
|
+
|
|
154
|
+
# Reduce tick label size
|
|
155
|
+
ax.tick_params(labelsize=7)
|
|
156
|
+
|
|
157
|
+
# Tight layout to maximize plot area
|
|
158
|
+
fig.tight_layout(pad=0.5)
|
|
159
|
+
|
|
160
|
+
return fig
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _decimate_uniform(signal: NDArray[np.float64], target_samples: int) -> NDArray[np.float64]:
|
|
164
|
+
"""Decimate signal to exactly target_samples using uniform stride.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
signal: Input signal
|
|
168
|
+
target_samples: Target number of samples
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Decimated signal with exactly target_samples
|
|
172
|
+
"""
|
|
173
|
+
if len(signal) <= target_samples:
|
|
174
|
+
return signal
|
|
175
|
+
|
|
176
|
+
# Calculate uniform stride
|
|
177
|
+
stride = len(signal) // target_samples
|
|
178
|
+
|
|
179
|
+
# Sample at uniform intervals
|
|
180
|
+
indices = np.arange(0, len(signal), stride)[:target_samples]
|
|
181
|
+
|
|
182
|
+
decimated: NDArray[np.float64] = signal[indices]
|
|
183
|
+
return decimated
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def render_thumbnail_multichannel(
|
|
187
|
+
signals: list[NDArray[np.float64]],
|
|
188
|
+
sample_rate: float,
|
|
189
|
+
*,
|
|
190
|
+
size: tuple[int, int] = (400, 300),
|
|
191
|
+
max_samples: int = 1000,
|
|
192
|
+
time_unit: str = "auto",
|
|
193
|
+
channel_names: list[str] | None = None,
|
|
194
|
+
dpi: int = 72,
|
|
195
|
+
) -> Figure:
|
|
196
|
+
"""Render fast preview thumbnail of multiple channels.
|
|
197
|
+
|
|
198
|
+
: Fast multi-channel preview rendering.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
signals: List of signal arrays
|
|
202
|
+
sample_rate: Sample rate in Hz
|
|
203
|
+
size: Thumbnail size in pixels (width, height)
|
|
204
|
+
max_samples: Maximum samples per channel
|
|
205
|
+
time_unit: Time unit for x-axis
|
|
206
|
+
channel_names: Optional channel names
|
|
207
|
+
dpi: DPI for rendering
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Matplotlib Figure object
|
|
211
|
+
|
|
212
|
+
Raises:
|
|
213
|
+
ValueError: If inputs are invalid
|
|
214
|
+
ImportError: If matplotlib is not available
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> signals = [ch1_data, ch2_data, ch3_data]
|
|
218
|
+
>>> fig = render_thumbnail_multichannel(signals, 1e6)
|
|
219
|
+
|
|
220
|
+
References:
|
|
221
|
+
VIS-018: Thumbnail Mode
|
|
222
|
+
"""
|
|
223
|
+
if not HAS_MATPLOTLIB:
|
|
224
|
+
raise ImportError("matplotlib is required for visualization")
|
|
225
|
+
|
|
226
|
+
if len(signals) == 0:
|
|
227
|
+
raise ValueError("Must provide at least one signal")
|
|
228
|
+
if sample_rate <= 0:
|
|
229
|
+
raise ValueError("Sample rate must be positive")
|
|
230
|
+
|
|
231
|
+
n_channels = len(signals)
|
|
232
|
+
|
|
233
|
+
if channel_names is None:
|
|
234
|
+
channel_names = [f"CH{i + 1}" for i in range(n_channels)]
|
|
235
|
+
|
|
236
|
+
# Configure matplotlib for fast rendering
|
|
237
|
+
with plt.rc_context(
|
|
238
|
+
{
|
|
239
|
+
"path.simplify": True,
|
|
240
|
+
"path.simplify_threshold": 1.0,
|
|
241
|
+
"agg.path.chunksize": 1000,
|
|
242
|
+
"lines.antialiased": False,
|
|
243
|
+
"patch.antialiased": False,
|
|
244
|
+
"text.antialiased": False,
|
|
245
|
+
}
|
|
246
|
+
):
|
|
247
|
+
# Calculate figure size
|
|
248
|
+
width_inches = size[0] / dpi
|
|
249
|
+
height_inches = size[1] / dpi
|
|
250
|
+
|
|
251
|
+
fig, axes = plt.subplots(
|
|
252
|
+
n_channels,
|
|
253
|
+
1,
|
|
254
|
+
figsize=(width_inches, height_inches),
|
|
255
|
+
dpi=dpi,
|
|
256
|
+
sharex=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if n_channels == 1:
|
|
260
|
+
axes = [axes]
|
|
261
|
+
|
|
262
|
+
# Get time unit from first signal
|
|
263
|
+
if len(signals[0]) > 0:
|
|
264
|
+
total_time = len(signals[0]) / sample_rate
|
|
265
|
+
if time_unit == "auto":
|
|
266
|
+
if total_time < 1e-6:
|
|
267
|
+
time_unit = "ns"
|
|
268
|
+
elif total_time < 1e-3:
|
|
269
|
+
time_unit = "us"
|
|
270
|
+
elif total_time < 1:
|
|
271
|
+
time_unit = "ms"
|
|
272
|
+
else:
|
|
273
|
+
time_unit = "s"
|
|
274
|
+
else:
|
|
275
|
+
time_unit = "s"
|
|
276
|
+
|
|
277
|
+
time_multipliers = {"s": 1.0, "ms": 1e3, "us": 1e6, "ns": 1e9}
|
|
278
|
+
multiplier = time_multipliers.get(time_unit, 1.0)
|
|
279
|
+
|
|
280
|
+
# Plot each channel
|
|
281
|
+
for i, (sig, name, ax) in enumerate(zip(signals, channel_names, axes, strict=False)):
|
|
282
|
+
if len(sig) == 0:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# Decimate signal
|
|
286
|
+
decimated = _decimate_uniform(sig, max_samples)
|
|
287
|
+
|
|
288
|
+
# Time vector
|
|
289
|
+
total_time = len(sig) / sample_rate
|
|
290
|
+
time = np.linspace(0, total_time, len(decimated)) * multiplier
|
|
291
|
+
|
|
292
|
+
# Plot
|
|
293
|
+
ax.plot(time, decimated, "b-", linewidth=0.5, antialiased=False)
|
|
294
|
+
|
|
295
|
+
# Channel label
|
|
296
|
+
ax.set_ylabel(name, fontsize=7, rotation=0, ha="right", va="center")
|
|
297
|
+
ax.tick_params(labelsize=6)
|
|
298
|
+
|
|
299
|
+
# Only x-label on bottom
|
|
300
|
+
if i == n_channels - 1:
|
|
301
|
+
ax.set_xlabel(f"Time ({time_unit})", fontsize=8)
|
|
302
|
+
|
|
303
|
+
fig.tight_layout(pad=0.3)
|
|
304
|
+
|
|
305
|
+
return fig
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
__all__ = [
|
|
309
|
+
"render_thumbnail",
|
|
310
|
+
"render_thumbnail_multichannel",
|
|
311
|
+
]
|