oscura 0.0.1__py3-none-any.whl → 0.1.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.
- 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.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.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.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Visualization layout functions for multi-channel plots and annotation placement.
|
|
2
|
+
|
|
3
|
+
This module provides intelligent layout algorithms for stacking multiple
|
|
4
|
+
channels and optimizing annotation placement with collision avoidance.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.visualization.layout import layout_stacked_channels
|
|
9
|
+
>>> layout = layout_stacked_channels(n_channels=4, figsize=(10, 8))
|
|
10
|
+
>>> print(f"Channel heights: {layout['heights']}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
- Force-directed graph layout (Fruchterman-Reingold)
|
|
14
|
+
- Constrained layout solver for equal spacing
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ChannelLayout:
|
|
30
|
+
"""Layout specification for stacked channels.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
n_channels: Number of channels to stack
|
|
34
|
+
heights: Array of subplot heights (normalized 0-1)
|
|
35
|
+
gaps: Array of gap sizes between channels (normalized 0-1)
|
|
36
|
+
y_positions: Array of Y positions for each channel (normalized 0-1)
|
|
37
|
+
shared_x: Whether channels share X-axis
|
|
38
|
+
figsize: Figure size (width, height) in inches
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
n_channels: int
|
|
42
|
+
heights: NDArray[np.float64]
|
|
43
|
+
gaps: NDArray[np.float64]
|
|
44
|
+
y_positions: NDArray[np.float64]
|
|
45
|
+
shared_x: bool
|
|
46
|
+
figsize: tuple[float, float]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Annotation:
|
|
51
|
+
"""Annotation specification with position and bounding box.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
text: Annotation text
|
|
55
|
+
x: X coordinate in data units
|
|
56
|
+
y: Y coordinate in data units
|
|
57
|
+
bbox_width: Bounding box width in display units
|
|
58
|
+
bbox_height: Bounding box height in display units
|
|
59
|
+
priority: Priority for placement (0-1, higher is more important)
|
|
60
|
+
anchor: Preferred anchor position ("top", "bottom", "left", "right", "auto")
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
text: str
|
|
64
|
+
x: float
|
|
65
|
+
y: float
|
|
66
|
+
bbox_width: float = 50.0
|
|
67
|
+
bbox_height: float = 20.0
|
|
68
|
+
priority: float = 0.5
|
|
69
|
+
anchor: str = "auto"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PlacedAnnotation:
|
|
74
|
+
"""Annotation with optimized placement.
|
|
75
|
+
|
|
76
|
+
Attributes:
|
|
77
|
+
annotation: Original annotation
|
|
78
|
+
display_x: Optimized X position in display units
|
|
79
|
+
display_y: Optimized Y position in display units
|
|
80
|
+
needs_leader: Whether a leader line is needed
|
|
81
|
+
leader_points: Points for leader line (if needed)
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
annotation: Annotation
|
|
85
|
+
display_x: float
|
|
86
|
+
display_y: float
|
|
87
|
+
needs_leader: bool
|
|
88
|
+
leader_points: list[tuple[float, float]] | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def layout_stacked_channels(
|
|
92
|
+
n_channels: int,
|
|
93
|
+
*,
|
|
94
|
+
figsize: tuple[float, float] = (10, 8),
|
|
95
|
+
gap_ratio: float = 0.1,
|
|
96
|
+
shared_x: bool = True,
|
|
97
|
+
) -> ChannelLayout:
|
|
98
|
+
"""Calculate equal vertical spacing for stacked multi-channel plots.
|
|
99
|
+
|
|
100
|
+
Implements constrained layout solver for equal spacing with configurable
|
|
101
|
+
gaps between channels, ensuring proper vertical alignment.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
n_channels: Number of channels to stack.
|
|
105
|
+
figsize: Figure size (width, height) in inches.
|
|
106
|
+
gap_ratio: Ratio of gap to channel height (default 0.1 = 10%).
|
|
107
|
+
shared_x: Whether channels share X-axis (affects bottom margin).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
ChannelLayout with heights, gaps, and positions.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ValueError: If n_channels < 1 or gap_ratio invalid.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> layout = layout_stacked_channels(n_channels=3, gap_ratio=0.1)
|
|
117
|
+
>>> print(f"Channel 0 position: {layout.y_positions[0]:.3f}")
|
|
118
|
+
|
|
119
|
+
References:
|
|
120
|
+
VIS-015: Multi-Channel Stack Optimization
|
|
121
|
+
"""
|
|
122
|
+
if n_channels < 1:
|
|
123
|
+
raise ValueError("n_channels must be >= 1")
|
|
124
|
+
|
|
125
|
+
if gap_ratio < 0 or gap_ratio > 1:
|
|
126
|
+
raise ValueError(f"gap_ratio must be in [0, 1], got {gap_ratio}")
|
|
127
|
+
|
|
128
|
+
# Total available height (normalized to 1.0)
|
|
129
|
+
# Reserve space for margins
|
|
130
|
+
top_margin = 0.05
|
|
131
|
+
bottom_margin = 0.1 if shared_x else 0.05
|
|
132
|
+
available_height = 1.0 - top_margin - bottom_margin
|
|
133
|
+
|
|
134
|
+
# Calculate channel height with gaps
|
|
135
|
+
# Total height = n_channels * h + (n_channels - 1) * gap
|
|
136
|
+
# where gap = gap_ratio * h
|
|
137
|
+
# Solving: available_height = n_channels * h + (n_channels - 1) * gap_ratio * h
|
|
138
|
+
# = h * (n_channels + (n_channels - 1) * gap_ratio)
|
|
139
|
+
denominator = n_channels + (n_channels - 1) * gap_ratio
|
|
140
|
+
channel_height = available_height / denominator
|
|
141
|
+
gap_height = channel_height * gap_ratio
|
|
142
|
+
|
|
143
|
+
# Calculate heights and gaps arrays
|
|
144
|
+
heights = np.full(n_channels, channel_height, dtype=np.float64)
|
|
145
|
+
gaps = np.full(n_channels - 1, gap_height, dtype=np.float64) if n_channels > 1 else np.array([])
|
|
146
|
+
|
|
147
|
+
# Calculate Y positions (from bottom)
|
|
148
|
+
y_positions = np.zeros(n_channels, dtype=np.float64)
|
|
149
|
+
current_y = bottom_margin
|
|
150
|
+
|
|
151
|
+
for i in range(n_channels):
|
|
152
|
+
# Channels are indexed from bottom to top
|
|
153
|
+
y_positions[i] = current_y
|
|
154
|
+
current_y += channel_height
|
|
155
|
+
if i < n_channels - 1:
|
|
156
|
+
current_y += gap_height
|
|
157
|
+
|
|
158
|
+
return ChannelLayout(
|
|
159
|
+
n_channels=n_channels,
|
|
160
|
+
heights=heights,
|
|
161
|
+
gaps=gaps,
|
|
162
|
+
y_positions=y_positions,
|
|
163
|
+
shared_x=shared_x,
|
|
164
|
+
figsize=figsize,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def optimize_annotation_placement(
|
|
169
|
+
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
|
+
) -> 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.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
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
|
+
|
|
191
|
+
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)
|
|
205
|
+
"""
|
|
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
|
+
placed = []
|
|
212
|
+
|
|
213
|
+
for annot in annotations:
|
|
214
|
+
# Initial placement at anchor point
|
|
215
|
+
placed.append(
|
|
216
|
+
PlacedAnnotation(
|
|
217
|
+
annotation=annot,
|
|
218
|
+
display_x=annot.x,
|
|
219
|
+
display_y=annot.y,
|
|
220
|
+
needs_leader=False,
|
|
221
|
+
leader_points=None,
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
|
|
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,
|
|
291
|
+
)
|
|
292
|
+
moved = True
|
|
293
|
+
|
|
294
|
+
# Converged if nothing moved
|
|
295
|
+
if not moved:
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
# Determine which annotations need leader lines
|
|
299
|
+
# (displaced beyond threshold from original position)
|
|
300
|
+
leader_threshold = 20.0 # pixels
|
|
301
|
+
|
|
302
|
+
for i, p in enumerate(placed):
|
|
303
|
+
anchor_x = p.annotation.x
|
|
304
|
+
anchor_y = p.annotation.y
|
|
305
|
+
|
|
306
|
+
displacement = np.sqrt((p.display_x - anchor_x) ** 2 + (p.display_y - anchor_y) ** 2)
|
|
307
|
+
|
|
308
|
+
if displacement > leader_threshold:
|
|
309
|
+
# Generate simple orthogonal leader line
|
|
310
|
+
leader_points = _generate_leader_line(
|
|
311
|
+
(anchor_x, anchor_y),
|
|
312
|
+
(p.display_x, p.display_y),
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
placed[i] = PlacedAnnotation(
|
|
316
|
+
annotation=p.annotation,
|
|
317
|
+
display_x=p.display_x,
|
|
318
|
+
display_y=p.display_y,
|
|
319
|
+
needs_leader=True,
|
|
320
|
+
leader_points=leader_points,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return placed
|
|
324
|
+
|
|
325
|
+
|
|
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.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
anchor: Anchor point (x, y)
|
|
334
|
+
label: Label position (x, y)
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
List of points for leader line
|
|
338
|
+
"""
|
|
339
|
+
ax, ay = anchor
|
|
340
|
+
lx, ly = label
|
|
341
|
+
|
|
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
|
|
345
|
+
|
|
346
|
+
dx = abs(lx - ax)
|
|
347
|
+
dy = abs(ly - ay)
|
|
348
|
+
|
|
349
|
+
if dx > dy:
|
|
350
|
+
# Horizontal-first
|
|
351
|
+
mid = (lx, ay)
|
|
352
|
+
else:
|
|
353
|
+
# Vertical-first
|
|
354
|
+
mid = (ax, ly)
|
|
355
|
+
|
|
356
|
+
return [anchor, mid, label]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
__all__ = [
|
|
360
|
+
"Annotation",
|
|
361
|
+
"ChannelLayout",
|
|
362
|
+
"PlacedAnnotation",
|
|
363
|
+
"layout_stacked_channels",
|
|
364
|
+
"optimize_annotation_placement",
|
|
365
|
+
]
|