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,296 @@
|
|
|
1
|
+
"""Multi-channel report generation for Oscura.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for generating reports across multiple channels
|
|
4
|
+
with channel comparison and aggregation.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.reporting.multichannel import generate_multichannel_report
|
|
9
|
+
>>> report = generate_multichannel_report(channel_results, "multi_report.pdf")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from oscura.reporting.core import Report, ReportConfig, Section
|
|
17
|
+
from oscura.reporting.tables import create_measurement_table
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_multichannel_report(
|
|
21
|
+
channel_results: dict[str, dict[str, Any]],
|
|
22
|
+
*,
|
|
23
|
+
title: str = "Multi-Channel Analysis Report",
|
|
24
|
+
compare_channels: bool = True,
|
|
25
|
+
aggregate_statistics: bool = True,
|
|
26
|
+
individual_sections: bool = True,
|
|
27
|
+
**kwargs: Any,
|
|
28
|
+
) -> Report:
|
|
29
|
+
"""Generate report for multi-channel analysis.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
channel_results: Dictionary mapping channel name to results.
|
|
33
|
+
title: Report title.
|
|
34
|
+
compare_channels: Include channel comparison section.
|
|
35
|
+
aggregate_statistics: Include aggregate statistics across channels.
|
|
36
|
+
individual_sections: Include individual channel sections.
|
|
37
|
+
**kwargs: Additional report configuration options.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Multi-channel Report object.
|
|
41
|
+
|
|
42
|
+
References:
|
|
43
|
+
REPORT-007
|
|
44
|
+
"""
|
|
45
|
+
config = ReportConfig(title=title, **kwargs)
|
|
46
|
+
report = Report(config=config)
|
|
47
|
+
|
|
48
|
+
# Add executive summary
|
|
49
|
+
summary_content = _generate_multichannel_summary(channel_results)
|
|
50
|
+
report.add_section("Executive Summary", summary_content, level=1)
|
|
51
|
+
|
|
52
|
+
# Add aggregate statistics
|
|
53
|
+
if aggregate_statistics:
|
|
54
|
+
stats_section = _create_aggregate_statistics_section(channel_results)
|
|
55
|
+
report.sections.append(stats_section)
|
|
56
|
+
|
|
57
|
+
# Add channel comparison
|
|
58
|
+
if compare_channels and len(channel_results) > 1:
|
|
59
|
+
comparison_section = _create_channel_comparison_section(channel_results)
|
|
60
|
+
report.sections.append(comparison_section)
|
|
61
|
+
|
|
62
|
+
# Add individual channel sections
|
|
63
|
+
if individual_sections:
|
|
64
|
+
for channel_name, results in channel_results.items():
|
|
65
|
+
channel_section = _create_channel_section(channel_name, results)
|
|
66
|
+
report.sections.append(channel_section)
|
|
67
|
+
|
|
68
|
+
return report
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _generate_multichannel_summary(channel_results: dict[str, dict[str, Any]]) -> str:
|
|
72
|
+
"""Generate summary for multi-channel report."""
|
|
73
|
+
summary_parts = []
|
|
74
|
+
|
|
75
|
+
total_channels = len(channel_results)
|
|
76
|
+
summary_parts.append(f"Analyzed {total_channels} channel(s).")
|
|
77
|
+
|
|
78
|
+
# Aggregate pass/fail across channels
|
|
79
|
+
total_tests = 0
|
|
80
|
+
total_passed = 0
|
|
81
|
+
|
|
82
|
+
for results in channel_results.values():
|
|
83
|
+
total_tests += results.get("total_count", 0)
|
|
84
|
+
total_passed += results.get("pass_count", 0)
|
|
85
|
+
|
|
86
|
+
if total_tests > 0:
|
|
87
|
+
total_failed = total_tests - total_passed
|
|
88
|
+
summary_parts.append(
|
|
89
|
+
f"\nOverall: {total_passed}/{total_tests} tests passed "
|
|
90
|
+
f"({total_passed / total_tests * 100:.1f}% pass rate)."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if total_failed > 0:
|
|
94
|
+
summary_parts.append(f"{total_failed} test(s) failed across all channels.")
|
|
95
|
+
|
|
96
|
+
# Channel-specific summary
|
|
97
|
+
failed_channels = []
|
|
98
|
+
for channel_name, results in channel_results.items():
|
|
99
|
+
pass_count = results.get("pass_count", 0)
|
|
100
|
+
total_count = results.get("total_count", 0)
|
|
101
|
+
if total_count > 0 and pass_count < total_count:
|
|
102
|
+
failed_channels.append(channel_name)
|
|
103
|
+
|
|
104
|
+
if failed_channels:
|
|
105
|
+
summary_parts.append(f"\nChannels with failures: {', '.join(failed_channels)}")
|
|
106
|
+
else:
|
|
107
|
+
summary_parts.append("\nAll channels passed all tests.")
|
|
108
|
+
|
|
109
|
+
return "\n".join(summary_parts)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _create_aggregate_statistics_section(
|
|
113
|
+
channel_results: dict[str, dict[str, Any]],
|
|
114
|
+
) -> Section:
|
|
115
|
+
"""Create aggregate statistics section across all channels."""
|
|
116
|
+
# Collect all measurement parameters
|
|
117
|
+
all_params = set()
|
|
118
|
+
for results in channel_results.values():
|
|
119
|
+
if "measurements" in results:
|
|
120
|
+
all_params.update(results["measurements"].keys())
|
|
121
|
+
|
|
122
|
+
# Build aggregate table
|
|
123
|
+
import numpy as np
|
|
124
|
+
|
|
125
|
+
headers = ["Parameter", "Min", "Mean", "Max", "Std Dev"]
|
|
126
|
+
rows = []
|
|
127
|
+
|
|
128
|
+
for param in sorted(all_params):
|
|
129
|
+
values = []
|
|
130
|
+
unit = ""
|
|
131
|
+
|
|
132
|
+
for results in channel_results.values():
|
|
133
|
+
if "measurements" in results and param in results["measurements"]:
|
|
134
|
+
meas = results["measurements"][param]
|
|
135
|
+
if "value" in meas and meas["value"] is not None:
|
|
136
|
+
values.append(meas["value"])
|
|
137
|
+
if not unit and "unit" in meas:
|
|
138
|
+
unit = meas["unit"]
|
|
139
|
+
|
|
140
|
+
if values:
|
|
141
|
+
from oscura.reporting.formatting import NumberFormatter
|
|
142
|
+
|
|
143
|
+
formatter = NumberFormatter()
|
|
144
|
+
rows.append(
|
|
145
|
+
[
|
|
146
|
+
param,
|
|
147
|
+
formatter.format(np.min(values), unit),
|
|
148
|
+
formatter.format(np.mean(values), unit),
|
|
149
|
+
formatter.format(np.max(values), unit),
|
|
150
|
+
formatter.format(np.std(values), unit),
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
table = {"type": "table", "headers": headers, "data": rows}
|
|
155
|
+
|
|
156
|
+
return Section(
|
|
157
|
+
title="Aggregate Statistics",
|
|
158
|
+
content=[table],
|
|
159
|
+
level=1,
|
|
160
|
+
visible=True,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _create_channel_comparison_section(
|
|
165
|
+
channel_results: dict[str, dict[str, Any]],
|
|
166
|
+
) -> Section:
|
|
167
|
+
"""Create channel-to-channel comparison section."""
|
|
168
|
+
from oscura.reporting.formatting import NumberFormatter
|
|
169
|
+
|
|
170
|
+
formatter = NumberFormatter()
|
|
171
|
+
|
|
172
|
+
# Build comparison table
|
|
173
|
+
channel_names = list(channel_results.keys())
|
|
174
|
+
headers = ["Parameter", *channel_names]
|
|
175
|
+
|
|
176
|
+
# Collect all parameters
|
|
177
|
+
all_params = set()
|
|
178
|
+
for results in channel_results.values():
|
|
179
|
+
if "measurements" in results:
|
|
180
|
+
all_params.update(results["measurements"].keys())
|
|
181
|
+
|
|
182
|
+
rows = []
|
|
183
|
+
for param in sorted(all_params):
|
|
184
|
+
row = [param]
|
|
185
|
+
|
|
186
|
+
for channel_name in channel_names:
|
|
187
|
+
results = channel_results[channel_name]
|
|
188
|
+
if "measurements" in results and param in results["measurements"]:
|
|
189
|
+
meas = results["measurements"][param]
|
|
190
|
+
value = meas.get("value")
|
|
191
|
+
unit = meas.get("unit", "")
|
|
192
|
+
if value is not None:
|
|
193
|
+
row.append(formatter.format(value, unit))
|
|
194
|
+
else:
|
|
195
|
+
row.append("-")
|
|
196
|
+
else:
|
|
197
|
+
row.append("-")
|
|
198
|
+
|
|
199
|
+
rows.append(row)
|
|
200
|
+
|
|
201
|
+
table = {"type": "table", "headers": headers, "data": rows}
|
|
202
|
+
|
|
203
|
+
return Section(
|
|
204
|
+
title="Channel Comparison",
|
|
205
|
+
content=[table],
|
|
206
|
+
level=1,
|
|
207
|
+
visible=True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _create_channel_section(
|
|
212
|
+
channel_name: str,
|
|
213
|
+
results: dict[str, Any],
|
|
214
|
+
) -> Section:
|
|
215
|
+
"""Create individual channel section."""
|
|
216
|
+
subsections = []
|
|
217
|
+
|
|
218
|
+
# Channel summary
|
|
219
|
+
summary_parts = []
|
|
220
|
+
if "pass_count" in results and "total_count" in results:
|
|
221
|
+
pass_count = results["pass_count"]
|
|
222
|
+
total = results["total_count"]
|
|
223
|
+
summary_parts.append(
|
|
224
|
+
f"{pass_count}/{total} tests passed ({pass_count / total * 100:.1f}% pass rate)."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Measurements
|
|
228
|
+
if "measurements" in results:
|
|
229
|
+
table = create_measurement_table(results["measurements"], format="dict")
|
|
230
|
+
subsections.append(
|
|
231
|
+
Section(
|
|
232
|
+
title="Measurements",
|
|
233
|
+
content=[table],
|
|
234
|
+
level=3,
|
|
235
|
+
visible=True,
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return Section(
|
|
240
|
+
title=f"Channel: {channel_name}",
|
|
241
|
+
content="\n".join(summary_parts) if summary_parts else "",
|
|
242
|
+
level=2,
|
|
243
|
+
visible=True,
|
|
244
|
+
subsections=subsections,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def create_channel_crosstalk_section(
|
|
249
|
+
crosstalk_results: dict[str, Any],
|
|
250
|
+
) -> Section:
|
|
251
|
+
"""Create channel crosstalk analysis section.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
crosstalk_results: Crosstalk analysis results between channels.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
Crosstalk Section object.
|
|
258
|
+
|
|
259
|
+
References:
|
|
260
|
+
REPORT-007
|
|
261
|
+
"""
|
|
262
|
+
from oscura.reporting.formatting import NumberFormatter
|
|
263
|
+
|
|
264
|
+
formatter = NumberFormatter()
|
|
265
|
+
|
|
266
|
+
if "crosstalk_matrix" in crosstalk_results:
|
|
267
|
+
matrix = crosstalk_results["crosstalk_matrix"]
|
|
268
|
+
channels = crosstalk_results.get("channels", [])
|
|
269
|
+
|
|
270
|
+
headers = ["Aggressor → Victim", *channels]
|
|
271
|
+
rows = []
|
|
272
|
+
|
|
273
|
+
for i, aggressor in enumerate(channels):
|
|
274
|
+
row = [aggressor]
|
|
275
|
+
for j, _victim in enumerate(channels):
|
|
276
|
+
if i == j:
|
|
277
|
+
row.append("-")
|
|
278
|
+
else:
|
|
279
|
+
crosstalk_db = matrix[i][j]
|
|
280
|
+
row.append(formatter.format(crosstalk_db, "dB"))
|
|
281
|
+
rows.append(row)
|
|
282
|
+
|
|
283
|
+
table = {"type": "table", "headers": headers, "data": rows}
|
|
284
|
+
content = [
|
|
285
|
+
"Channel-to-channel crosstalk measurements:\n",
|
|
286
|
+
table,
|
|
287
|
+
]
|
|
288
|
+
else:
|
|
289
|
+
content = "No crosstalk analysis available." # type: ignore[assignment]
|
|
290
|
+
|
|
291
|
+
return Section(
|
|
292
|
+
title="Channel Crosstalk Analysis",
|
|
293
|
+
content=content,
|
|
294
|
+
level=2,
|
|
295
|
+
visible=True,
|
|
296
|
+
)
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Output management for comprehensive analysis reports.
|
|
2
|
+
|
|
3
|
+
This module provides directory structure and file management for analysis
|
|
4
|
+
report outputs, including plots, JSON/YAML data exports, and logs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
from oscura.reporting.config import AnalysisDomain # noqa: TC001
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _sanitize_for_serialization(obj: Any, max_depth: int = 10) -> Any:
|
|
21
|
+
"""Convert non-serializable objects for JSON/YAML output.
|
|
22
|
+
|
|
23
|
+
Handles generators, numpy arrays, and other problematic types
|
|
24
|
+
that can appear in analysis results.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
obj: Object to sanitize.
|
|
28
|
+
max_depth: Maximum recursion depth to prevent infinite loops.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Serialization-safe version of the object.
|
|
32
|
+
"""
|
|
33
|
+
import types
|
|
34
|
+
|
|
35
|
+
from oscura.core.types import DigitalTrace, TraceMetadata, WaveformTrace
|
|
36
|
+
|
|
37
|
+
if max_depth <= 0:
|
|
38
|
+
return "<max depth exceeded>"
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Don't sanitize Oscura types - let the JSONEncoder handle them
|
|
42
|
+
if isinstance(obj, WaveformTrace | DigitalTrace | TraceMetadata):
|
|
43
|
+
return obj
|
|
44
|
+
if isinstance(obj, dict):
|
|
45
|
+
# Sanitize both keys and values, convert non-string keys to strings
|
|
46
|
+
sanitized = {}
|
|
47
|
+
for k, v in obj.items():
|
|
48
|
+
# Convert bytes keys to hex strings
|
|
49
|
+
if isinstance(k, bytes):
|
|
50
|
+
k = f"0x{k.hex()}"
|
|
51
|
+
# Convert other non-string keys to strings
|
|
52
|
+
elif not isinstance(k, str | int | float | bool | type(None)):
|
|
53
|
+
k = str(k)
|
|
54
|
+
sanitized[k] = _sanitize_for_serialization(v, max_depth - 1)
|
|
55
|
+
return sanitized
|
|
56
|
+
elif isinstance(obj, list | tuple):
|
|
57
|
+
return [_sanitize_for_serialization(item, max_depth - 1) for item in obj]
|
|
58
|
+
elif isinstance(obj, types.GeneratorType):
|
|
59
|
+
# Convert generators to lists, but catch errors
|
|
60
|
+
try:
|
|
61
|
+
items = list(obj)
|
|
62
|
+
return [_sanitize_for_serialization(item, max_depth - 1) for item in items]
|
|
63
|
+
except Exception:
|
|
64
|
+
# Return None for incompatible generators (cleaner than error string)
|
|
65
|
+
return None
|
|
66
|
+
elif isinstance(obj, np.ndarray):
|
|
67
|
+
# Limit large arrays
|
|
68
|
+
if obj.size > 10000:
|
|
69
|
+
return f"<ndarray shape={obj.shape} dtype={obj.dtype}>"
|
|
70
|
+
return obj.tolist()
|
|
71
|
+
elif isinstance(obj, np.generic):
|
|
72
|
+
# Catch all numpy scalar types (int, float, complex, bool, str, etc.)
|
|
73
|
+
# This includes np.integer, np.floating, np.bool_, np.complexfloating, etc.
|
|
74
|
+
return obj.item()
|
|
75
|
+
elif isinstance(obj, np.integer | np.floating):
|
|
76
|
+
# Redundant but kept for clarity
|
|
77
|
+
return obj.item()
|
|
78
|
+
elif isinstance(obj, np.bool_):
|
|
79
|
+
# Redundant but kept for clarity
|
|
80
|
+
return bool(obj)
|
|
81
|
+
elif isinstance(obj, float):
|
|
82
|
+
# Handle Python float inf/nan (not caught by JSONEncoder.default)
|
|
83
|
+
import math
|
|
84
|
+
|
|
85
|
+
if math.isinf(obj) or math.isnan(obj):
|
|
86
|
+
return None
|
|
87
|
+
return obj
|
|
88
|
+
elif isinstance(obj, complex):
|
|
89
|
+
# Handle complex numbers with inf/nan components
|
|
90
|
+
import math
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
math.isinf(obj.real)
|
|
94
|
+
or math.isnan(obj.real)
|
|
95
|
+
or math.isinf(obj.imag)
|
|
96
|
+
or math.isnan(obj.imag)
|
|
97
|
+
):
|
|
98
|
+
return None
|
|
99
|
+
return {"real": obj.real, "imag": obj.imag}
|
|
100
|
+
elif isinstance(obj, bytes):
|
|
101
|
+
# Limit large byte sequences
|
|
102
|
+
if len(obj) > 1000:
|
|
103
|
+
return f"<bytes len={len(obj)}>"
|
|
104
|
+
return obj.hex()
|
|
105
|
+
elif hasattr(obj, "__dict__") and not isinstance(obj, type):
|
|
106
|
+
# Convert dataclasses and objects to dicts
|
|
107
|
+
try:
|
|
108
|
+
return {
|
|
109
|
+
k: _sanitize_for_serialization(v, max_depth - 1)
|
|
110
|
+
for k, v in obj.__dict__.items()
|
|
111
|
+
}
|
|
112
|
+
except Exception:
|
|
113
|
+
return str(obj)
|
|
114
|
+
elif callable(obj):
|
|
115
|
+
return f"<callable: {getattr(obj, '__name__', str(obj))}>"
|
|
116
|
+
else:
|
|
117
|
+
# Try to convert to string as last resort
|
|
118
|
+
try:
|
|
119
|
+
return obj
|
|
120
|
+
except Exception:
|
|
121
|
+
return str(obj)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return f"<error: {type(e).__name__}: {str(e)[:50]}>"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class OutputManager:
|
|
127
|
+
"""Manages output directory structure and file operations for analysis reports.
|
|
128
|
+
|
|
129
|
+
Creates timestamped output directories with organized subdirectories for
|
|
130
|
+
different types of analysis outputs (plots, data files, logs, errors).
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
root: Root directory path for this analysis output.
|
|
134
|
+
timestamp: Timestamp for this output session.
|
|
135
|
+
timestamp_str: Formatted timestamp string.
|
|
136
|
+
|
|
137
|
+
Requirements:
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
base_dir: Path,
|
|
143
|
+
input_name: str,
|
|
144
|
+
timestamp: datetime | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Initialize output manager.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
base_dir: Base directory for all outputs.
|
|
150
|
+
input_name: Name of the input file/dataset being analyzed.
|
|
151
|
+
timestamp: Timestamp for this session (defaults to now).
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
>>> manager = OutputManager(Path("/output"), "signal_data")
|
|
155
|
+
>>> manager.root.name
|
|
156
|
+
'20260101_120000_signal_data_analysis'
|
|
157
|
+
"""
|
|
158
|
+
self._timestamp = timestamp or datetime.now()
|
|
159
|
+
self._timestamp_str = self._timestamp.strftime("%Y%m%d_%H%M%S")
|
|
160
|
+
|
|
161
|
+
# Create timestamped directory name
|
|
162
|
+
dirname = f"{self._timestamp_str}_{input_name}_analysis"
|
|
163
|
+
self._root = base_dir / dirname
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def root(self) -> Path:
|
|
167
|
+
"""Root directory path for this analysis output."""
|
|
168
|
+
return self._root
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def timestamp(self) -> datetime:
|
|
172
|
+
"""Timestamp for this output session."""
|
|
173
|
+
return self._timestamp
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def timestamp_str(self) -> str:
|
|
177
|
+
"""Formatted timestamp string (YYYYMMDD_HHMMSS)."""
|
|
178
|
+
return self._timestamp_str
|
|
179
|
+
|
|
180
|
+
def create(self) -> Path:
|
|
181
|
+
"""Create output directory structure.
|
|
182
|
+
|
|
183
|
+
Creates the root directory and standard subdirectories:
|
|
184
|
+
- plots/: Visualization outputs
|
|
185
|
+
- errors/: Error logs and diagnostics
|
|
186
|
+
- logs/: Analysis logs
|
|
187
|
+
- input/: Input file copies/metadata
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path to the created root directory.
|
|
191
|
+
|
|
192
|
+
Note:
|
|
193
|
+
This method is idempotent - calling it multiple times is safe.
|
|
194
|
+
|
|
195
|
+
Requirements:
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
199
|
+
>>> root = manager.create()
|
|
200
|
+
>>> (root / "plots").exists()
|
|
201
|
+
True
|
|
202
|
+
"""
|
|
203
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
# Create standard subdirectories
|
|
206
|
+
subdirs = ["plots", "errors", "logs", "input"]
|
|
207
|
+
for subdir in subdirs:
|
|
208
|
+
(self._root / subdir).mkdir(exist_ok=True)
|
|
209
|
+
|
|
210
|
+
return self._root
|
|
211
|
+
|
|
212
|
+
def create_domain_dir(self, domain: AnalysisDomain) -> Path:
|
|
213
|
+
"""Create and return domain-specific subdirectory.
|
|
214
|
+
|
|
215
|
+
Creates a subdirectory for organizing outputs from a specific
|
|
216
|
+
analysis domain (e.g., spectral/, digital/, jitter/).
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
domain: Analysis domain.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Path to the created domain directory.
|
|
223
|
+
|
|
224
|
+
Requirements:
|
|
225
|
+
|
|
226
|
+
Examples:
|
|
227
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
228
|
+
>>> manager.create()
|
|
229
|
+
>>> domain_dir = manager.create_domain_dir(AnalysisDomain.SPECTRAL)
|
|
230
|
+
>>> domain_dir.name
|
|
231
|
+
'spectral'
|
|
232
|
+
"""
|
|
233
|
+
domain_dir = self._root / domain.value
|
|
234
|
+
domain_dir.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
return domain_dir
|
|
236
|
+
|
|
237
|
+
def save_json(
|
|
238
|
+
self,
|
|
239
|
+
name: str,
|
|
240
|
+
data: dict[str, Any],
|
|
241
|
+
subdir: str | None = None,
|
|
242
|
+
) -> Path:
|
|
243
|
+
"""Save data as JSON file with pretty formatting.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
name: Filename (without .json extension).
|
|
247
|
+
data: Dictionary to serialize.
|
|
248
|
+
subdir: Optional subdirectory within root.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Path to the saved JSON file.
|
|
252
|
+
|
|
253
|
+
Requirements:
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
257
|
+
>>> manager.create()
|
|
258
|
+
>>> path = manager.save_json("metrics", {"snr": 42.5})
|
|
259
|
+
>>> path.name
|
|
260
|
+
'metrics.json'
|
|
261
|
+
"""
|
|
262
|
+
target_dir = self._root / subdir if subdir else self._root
|
|
263
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
|
|
265
|
+
filepath = target_dir / f"{name}.json"
|
|
266
|
+
with filepath.open("w") as f:
|
|
267
|
+
json.dump(data, f, indent=2, default=str)
|
|
268
|
+
|
|
269
|
+
return filepath
|
|
270
|
+
|
|
271
|
+
def save_yaml(
|
|
272
|
+
self,
|
|
273
|
+
name: str,
|
|
274
|
+
data: dict[str, Any],
|
|
275
|
+
subdir: str | None = None,
|
|
276
|
+
) -> Path:
|
|
277
|
+
"""Save data as YAML file.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
name: Filename (without .yaml extension).
|
|
281
|
+
data: Dictionary to serialize.
|
|
282
|
+
subdir: Optional subdirectory within root.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Path to the saved YAML file.
|
|
286
|
+
|
|
287
|
+
Requirements:
|
|
288
|
+
|
|
289
|
+
Examples:
|
|
290
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
291
|
+
>>> manager.create()
|
|
292
|
+
>>> path = manager.save_yaml("config", {"enabled": True})
|
|
293
|
+
>>> path.name
|
|
294
|
+
'config.yaml'
|
|
295
|
+
"""
|
|
296
|
+
target_dir = self._root / subdir if subdir else self._root
|
|
297
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
298
|
+
|
|
299
|
+
filepath = target_dir / f"{name}.yaml"
|
|
300
|
+
# Sanitize data to handle generators, numpy arrays, etc.
|
|
301
|
+
sanitized_data = _sanitize_for_serialization(data)
|
|
302
|
+
with filepath.open("w") as f:
|
|
303
|
+
yaml.dump(sanitized_data, f, default_flow_style=False, sort_keys=False)
|
|
304
|
+
|
|
305
|
+
return filepath
|
|
306
|
+
|
|
307
|
+
def save_plot(
|
|
308
|
+
self,
|
|
309
|
+
domain: AnalysisDomain,
|
|
310
|
+
name: str,
|
|
311
|
+
fig: Any,
|
|
312
|
+
format: str = "png",
|
|
313
|
+
dpi: int = 150,
|
|
314
|
+
) -> Path:
|
|
315
|
+
"""Save matplotlib figure to plots directory.
|
|
316
|
+
|
|
317
|
+
Saves plot with domain-prefixed filename in the plots/ subdirectory.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
domain: Analysis domain for this plot.
|
|
321
|
+
name: Plot name (without extension).
|
|
322
|
+
fig: Matplotlib figure object.
|
|
323
|
+
format: Image format (png, pdf, svg, etc.).
|
|
324
|
+
dpi: Resolution in dots per inch.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Path to the saved plot file.
|
|
328
|
+
|
|
329
|
+
Requirements:
|
|
330
|
+
|
|
331
|
+
Examples:
|
|
332
|
+
>>> import matplotlib.pyplot as plt
|
|
333
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
334
|
+
>>> manager.create()
|
|
335
|
+
>>> fig, ax = plt.subplots()
|
|
336
|
+
>>> path = manager.save_plot(AnalysisDomain.SPECTRAL, "fft", fig)
|
|
337
|
+
>>> path.name
|
|
338
|
+
'spectral_fft.png'
|
|
339
|
+
"""
|
|
340
|
+
plots_dir = self._root / "plots"
|
|
341
|
+
plots_dir.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
|
|
343
|
+
filename = f"{domain.value}_{name}.{format}"
|
|
344
|
+
filepath = plots_dir / filename
|
|
345
|
+
|
|
346
|
+
fig.savefig(filepath, format=format, dpi=dpi, bbox_inches="tight")
|
|
347
|
+
|
|
348
|
+
return filepath
|
|
349
|
+
|
|
350
|
+
def save_text(
|
|
351
|
+
self,
|
|
352
|
+
name: str,
|
|
353
|
+
content: str,
|
|
354
|
+
subdir: str | None = None,
|
|
355
|
+
) -> Path:
|
|
356
|
+
"""Save text content to file.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
name: Filename (with extension).
|
|
360
|
+
content: Text content to write.
|
|
361
|
+
subdir: Optional subdirectory within root.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Path to the saved text file.
|
|
365
|
+
|
|
366
|
+
Examples:
|
|
367
|
+
>>> manager = OutputManager(Path("/tmp/output"), "test")
|
|
368
|
+
>>> manager.create()
|
|
369
|
+
>>> path = manager.save_text("summary.txt", "Analysis complete")
|
|
370
|
+
>>> path.name
|
|
371
|
+
'summary.txt'
|
|
372
|
+
"""
|
|
373
|
+
target_dir = self._root / subdir if subdir else self._root
|
|
374
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
375
|
+
|
|
376
|
+
filepath = target_dir / name
|
|
377
|
+
filepath.write_text(content)
|
|
378
|
+
|
|
379
|
+
return filepath
|