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,615 @@
|
|
|
1
|
+
"""Batch report generation for TraceKit.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for generating reports across multiple DUTs
|
|
4
|
+
or files with summary reports and yield analysis.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.reporting.batch import batch_report, generate_batch_report
|
|
9
|
+
>>> # Process multiple files
|
|
10
|
+
>>> results = batch_report(['dut1.wfm', 'dut2.wfm'], template='production', output_dir='reports/')
|
|
11
|
+
>>> # Or generate from pre-computed results
|
|
12
|
+
>>> report = generate_batch_report(batch_results, "batch_summary.pdf")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from oscura.reporting.core import Report, ReportConfig, Section
|
|
24
|
+
from oscura.reporting.tables import format_batch_summary_table
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from collections.abc import Callable
|
|
28
|
+
|
|
29
|
+
from numpy.typing import NDArray
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"BatchReportResult",
|
|
35
|
+
"aggregate_batch_measurements",
|
|
36
|
+
"batch_report",
|
|
37
|
+
"generate_batch_report",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BatchReportResult:
|
|
42
|
+
"""Result of batch report generation.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
summary_report_path: Path to summary report
|
|
46
|
+
individual_report_paths: Paths to individual DUT reports
|
|
47
|
+
total_duts: Total number of DUTs processed
|
|
48
|
+
passed_duts: Number of passing DUTs
|
|
49
|
+
failed_duts: Number of failing DUTs
|
|
50
|
+
errors: List of processing errors
|
|
51
|
+
|
|
52
|
+
References:
|
|
53
|
+
RPT-003: Batch Report Generation
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
"""Initialize batch result."""
|
|
58
|
+
self.summary_report_path: Path | None = None
|
|
59
|
+
self.individual_report_paths: list[Path] = []
|
|
60
|
+
self.total_duts: int = 0
|
|
61
|
+
self.passed_duts: int = 0
|
|
62
|
+
self.failed_duts: int = 0
|
|
63
|
+
self.errors: list[tuple[str, str]] = []
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def dut_yield(self) -> float:
|
|
67
|
+
"""Calculate DUT yield percentage."""
|
|
68
|
+
if self.total_duts == 0:
|
|
69
|
+
return 0.0
|
|
70
|
+
return (self.passed_duts / self.total_duts) * 100
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def batch_report(
|
|
74
|
+
files: list[str | Path],
|
|
75
|
+
template: str = "production",
|
|
76
|
+
output_dir: str | Path = "reports",
|
|
77
|
+
*,
|
|
78
|
+
analyzer: Callable[[Any], dict[str, Any]] | None = None,
|
|
79
|
+
generate_individual: bool = True,
|
|
80
|
+
generate_summary: bool = True,
|
|
81
|
+
output_format: str = "pdf",
|
|
82
|
+
file_pattern: str = "{dut_id}_report.{ext}",
|
|
83
|
+
summary_filename: str = "batch_summary.{ext}",
|
|
84
|
+
dut_id_extractor: Callable[[Path], str] | None = None,
|
|
85
|
+
) -> BatchReportResult:
|
|
86
|
+
"""Generate reports for multiple DUTs/files.
|
|
87
|
+
|
|
88
|
+
This is the primary interface for batch report generation.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
files: List of input file paths
|
|
92
|
+
template: Report template name (default: 'production')
|
|
93
|
+
output_dir: Output directory for reports
|
|
94
|
+
analyzer: Optional analysis function (trace -> results dict)
|
|
95
|
+
generate_individual: Generate individual DUT reports
|
|
96
|
+
generate_summary: Generate summary report across all DUTs
|
|
97
|
+
output_format: Output format ('pdf', 'html')
|
|
98
|
+
file_pattern: Filename pattern for individual reports
|
|
99
|
+
summary_filename: Filename for summary report
|
|
100
|
+
dut_id_extractor: Function to extract DUT ID from file path
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
BatchReportResult with paths and statistics
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
>>> result = batch_report(
|
|
107
|
+
... files=['dut1.wfm', 'dut2.wfm', 'dut3.wfm'],
|
|
108
|
+
... template='production',
|
|
109
|
+
... output_dir='./reports'
|
|
110
|
+
... )
|
|
111
|
+
>>> print(f"Yield: {result.dut_yield:.1f}%")
|
|
112
|
+
>>> print(f"Summary: {result.summary_report_path}")
|
|
113
|
+
|
|
114
|
+
References:
|
|
115
|
+
RPT-003: Batch Report Generation
|
|
116
|
+
"""
|
|
117
|
+
import oscura as tk
|
|
118
|
+
from oscura.reporting.template_system import load_template
|
|
119
|
+
|
|
120
|
+
output_path = Path(output_dir)
|
|
121
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
result = BatchReportResult()
|
|
124
|
+
batch_results: list[dict[str, Any]] = []
|
|
125
|
+
|
|
126
|
+
# Load template
|
|
127
|
+
try:
|
|
128
|
+
report_template = load_template(template)
|
|
129
|
+
except ValueError:
|
|
130
|
+
logger.warning(f"Template '{template}' not found, using 'default'")
|
|
131
|
+
report_template = load_template("default")
|
|
132
|
+
|
|
133
|
+
# Default DUT ID extractor
|
|
134
|
+
if dut_id_extractor is None:
|
|
135
|
+
|
|
136
|
+
def dut_id_extractor(p: Path) -> str:
|
|
137
|
+
return Path(p).stem
|
|
138
|
+
|
|
139
|
+
# Process each file
|
|
140
|
+
for file_path in files:
|
|
141
|
+
file_path = Path(file_path)
|
|
142
|
+
dut_id = dut_id_extractor(file_path)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Load trace
|
|
146
|
+
trace = tk.load(str(file_path))
|
|
147
|
+
|
|
148
|
+
# Run analysis
|
|
149
|
+
if analyzer is not None:
|
|
150
|
+
dut_result = analyzer(trace)
|
|
151
|
+
else:
|
|
152
|
+
# Default analysis
|
|
153
|
+
dut_result = _default_analysis(trace)
|
|
154
|
+
|
|
155
|
+
# Add DUT metadata
|
|
156
|
+
dut_result["dut_id"] = dut_id
|
|
157
|
+
dut_result["source_file"] = str(file_path)
|
|
158
|
+
|
|
159
|
+
batch_results.append(dut_result)
|
|
160
|
+
|
|
161
|
+
# Check pass/fail
|
|
162
|
+
if dut_result.get("pass_count", 0) == dut_result.get("total_count", 0):
|
|
163
|
+
result.passed_duts += 1
|
|
164
|
+
else:
|
|
165
|
+
result.failed_duts += 1
|
|
166
|
+
|
|
167
|
+
result.total_duts += 1
|
|
168
|
+
|
|
169
|
+
# Generate individual report if requested
|
|
170
|
+
if generate_individual:
|
|
171
|
+
ext = output_format.lower()
|
|
172
|
+
individual_filename = file_pattern.format(dut_id=dut_id, ext=ext)
|
|
173
|
+
individual_path = output_path / individual_filename
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
_generate_individual_report(
|
|
177
|
+
dut_result, individual_path, report_template, output_format
|
|
178
|
+
)
|
|
179
|
+
result.individual_report_paths.append(individual_path)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Failed to generate report for {dut_id}: {e}")
|
|
182
|
+
result.errors.append((dut_id, str(e)))
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to process {file_path}: {e}")
|
|
186
|
+
result.errors.append((str(file_path), str(e)))
|
|
187
|
+
|
|
188
|
+
# Generate summary report if requested
|
|
189
|
+
if generate_summary and batch_results:
|
|
190
|
+
ext = output_format.lower()
|
|
191
|
+
summary_path = output_path / summary_filename.format(ext=ext)
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
summary_report = generate_batch_report(batch_results)
|
|
195
|
+
_save_report(summary_report, summary_path, output_format)
|
|
196
|
+
result.summary_report_path = summary_path
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"Failed to generate summary report: {e}")
|
|
199
|
+
result.errors.append(("summary", str(e)))
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _default_analysis(trace: Any) -> dict[str, Any]:
|
|
205
|
+
"""Default analysis for a trace."""
|
|
206
|
+
import numpy as np
|
|
207
|
+
|
|
208
|
+
from oscura.core.types import WaveformTrace
|
|
209
|
+
|
|
210
|
+
if not isinstance(trace, WaveformTrace):
|
|
211
|
+
return {
|
|
212
|
+
"measurements": {},
|
|
213
|
+
"pass_count": 0,
|
|
214
|
+
"total_count": 0,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
data = trace.data
|
|
218
|
+
|
|
219
|
+
# Basic measurements
|
|
220
|
+
measurements = {
|
|
221
|
+
"peak_to_peak": {
|
|
222
|
+
"value": float(np.ptp(data)),
|
|
223
|
+
"unit": "V",
|
|
224
|
+
"passed": True,
|
|
225
|
+
},
|
|
226
|
+
"rms": {
|
|
227
|
+
"value": float(np.sqrt(np.mean(data**2))),
|
|
228
|
+
"unit": "V",
|
|
229
|
+
"passed": True,
|
|
230
|
+
},
|
|
231
|
+
"mean": {
|
|
232
|
+
"value": float(np.mean(data)),
|
|
233
|
+
"unit": "V",
|
|
234
|
+
"passed": True,
|
|
235
|
+
},
|
|
236
|
+
"std_dev": {
|
|
237
|
+
"value": float(np.std(data)),
|
|
238
|
+
"unit": "V",
|
|
239
|
+
"passed": True,
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"measurements": measurements,
|
|
245
|
+
"pass_count": len(measurements),
|
|
246
|
+
"total_count": len(measurements),
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _generate_individual_report(
|
|
251
|
+
dut_result: dict[str, Any],
|
|
252
|
+
output_path: Path,
|
|
253
|
+
template: Any,
|
|
254
|
+
output_format: str,
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Generate individual DUT report."""
|
|
257
|
+
from oscura.reporting.core import Report, ReportConfig
|
|
258
|
+
|
|
259
|
+
dut_id = dut_result.get("dut_id", "Unknown")
|
|
260
|
+
config = ReportConfig(title=f"Test Report: {dut_id}")
|
|
261
|
+
report = Report(config=config)
|
|
262
|
+
|
|
263
|
+
# Add summary section
|
|
264
|
+
pass_count = dut_result.get("pass_count", 0)
|
|
265
|
+
total_count = dut_result.get("total_count", 0)
|
|
266
|
+
pass_rate = (pass_count / total_count * 100) if total_count > 0 else 0
|
|
267
|
+
|
|
268
|
+
summary = f"DUT: {dut_id}\n"
|
|
269
|
+
summary += f"Source: {dut_result.get('source_file', 'N/A')}\n"
|
|
270
|
+
summary += f"Pass Rate: {pass_count}/{total_count} ({pass_rate:.1f}%)"
|
|
271
|
+
|
|
272
|
+
report.add_section("Summary", summary, level=1)
|
|
273
|
+
|
|
274
|
+
# Add measurements
|
|
275
|
+
if "measurements" in dut_result:
|
|
276
|
+
from oscura.reporting.tables import create_measurement_table
|
|
277
|
+
|
|
278
|
+
table = create_measurement_table(dut_result["measurements"], format="dict")
|
|
279
|
+
report.add_section("Measurements", [table], level=1)
|
|
280
|
+
|
|
281
|
+
_save_report(report, output_path, output_format)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _save_report(report: Report, output_path: Path, output_format: str) -> None:
|
|
285
|
+
"""Save report in specified format."""
|
|
286
|
+
if output_format.lower() == "pdf":
|
|
287
|
+
from oscura.reporting.pdf import save_pdf_report
|
|
288
|
+
|
|
289
|
+
save_pdf_report(report, str(output_path))
|
|
290
|
+
elif output_format.lower() == "html":
|
|
291
|
+
from oscura.reporting.html import save_html_report
|
|
292
|
+
|
|
293
|
+
save_html_report(report, str(output_path))
|
|
294
|
+
else:
|
|
295
|
+
raise ValueError(f"Unsupported output format: {output_format}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def generate_batch_report(
|
|
299
|
+
batch_results: list[dict[str, Any]],
|
|
300
|
+
*,
|
|
301
|
+
title: str = "Batch Test Summary Report",
|
|
302
|
+
include_individual: bool = True,
|
|
303
|
+
include_yield_analysis: bool = True,
|
|
304
|
+
include_outliers: bool = True,
|
|
305
|
+
**kwargs: Any,
|
|
306
|
+
) -> Report:
|
|
307
|
+
"""Generate batch summary report for multiple DUTs.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
batch_results: List of result dictionaries, one per DUT.
|
|
311
|
+
title: Report title.
|
|
312
|
+
include_individual: Include individual DUT sections.
|
|
313
|
+
include_yield_analysis: Include yield analysis section.
|
|
314
|
+
include_outliers: Include outlier detection section.
|
|
315
|
+
**kwargs: Additional report configuration options.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Batch Report object.
|
|
319
|
+
|
|
320
|
+
References:
|
|
321
|
+
REPORT-009, REPORT-018
|
|
322
|
+
"""
|
|
323
|
+
config = ReportConfig(title=title, **kwargs)
|
|
324
|
+
report = Report(config=config)
|
|
325
|
+
|
|
326
|
+
# Add batch summary
|
|
327
|
+
summary = _generate_batch_summary(batch_results)
|
|
328
|
+
report.add_section("Batch Summary", summary, level=1)
|
|
329
|
+
|
|
330
|
+
# Add batch summary table
|
|
331
|
+
summary_table = format_batch_summary_table(batch_results, format="dict")
|
|
332
|
+
report.add_section("DUT Summary Table", [summary_table], level=1)
|
|
333
|
+
|
|
334
|
+
# Add yield analysis
|
|
335
|
+
if include_yield_analysis:
|
|
336
|
+
yield_section = _create_yield_analysis_section(batch_results)
|
|
337
|
+
report.sections.append(yield_section)
|
|
338
|
+
|
|
339
|
+
# Add statistical analysis
|
|
340
|
+
stats_section = _create_batch_statistics_section(batch_results)
|
|
341
|
+
report.sections.append(stats_section)
|
|
342
|
+
|
|
343
|
+
# Add outlier detection
|
|
344
|
+
if include_outliers:
|
|
345
|
+
outlier_section = _create_outlier_detection_section(batch_results)
|
|
346
|
+
report.sections.append(outlier_section)
|
|
347
|
+
|
|
348
|
+
# Add individual DUT sections
|
|
349
|
+
if include_individual:
|
|
350
|
+
for i, dut_result in enumerate(batch_results):
|
|
351
|
+
dut_section = _create_dut_section(dut_result, i)
|
|
352
|
+
report.sections.append(dut_section)
|
|
353
|
+
|
|
354
|
+
return report
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _generate_batch_summary(batch_results: list[dict[str, Any]]) -> str:
|
|
358
|
+
"""Generate batch summary text."""
|
|
359
|
+
summary_parts = []
|
|
360
|
+
|
|
361
|
+
total_duts = len(batch_results)
|
|
362
|
+
summary_parts.append(f"Tested {total_duts} DUT(s).")
|
|
363
|
+
|
|
364
|
+
# Aggregate statistics
|
|
365
|
+
total_tests = sum(r.get("total_count", 0) for r in batch_results)
|
|
366
|
+
total_passed = sum(r.get("pass_count", 0) for r in batch_results)
|
|
367
|
+
|
|
368
|
+
if total_tests > 0:
|
|
369
|
+
pass_rate = total_passed / total_tests * 100
|
|
370
|
+
summary_parts.append(
|
|
371
|
+
f"\nOverall: {total_passed}/{total_tests} tests passed ({pass_rate:.1f}% pass rate)."
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# DUT-level yield
|
|
375
|
+
passing_duts = sum(
|
|
376
|
+
1 for r in batch_results if r.get("pass_count", 0) == r.get("total_count", 0)
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
if total_duts > 0:
|
|
380
|
+
dut_yield = passing_duts / total_duts * 100
|
|
381
|
+
summary_parts.append(
|
|
382
|
+
f"\nDUT Yield: {passing_duts}/{total_duts} DUTs passed all tests "
|
|
383
|
+
f"({dut_yield:.1f}% yield)."
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Failed DUTs
|
|
387
|
+
if passing_duts < total_duts:
|
|
388
|
+
failed_duts = []
|
|
389
|
+
for i, r in enumerate(batch_results):
|
|
390
|
+
dut_id = r.get("dut_id", f"DUT-{i + 1}")
|
|
391
|
+
if r.get("pass_count", 0) < r.get("total_count", 0):
|
|
392
|
+
failed_duts.append(dut_id)
|
|
393
|
+
|
|
394
|
+
summary_parts.append(f"\nFailed DUTs: {', '.join(failed_duts)}")
|
|
395
|
+
|
|
396
|
+
return "\n".join(summary_parts)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _create_yield_analysis_section(batch_results: list[dict[str, Any]]) -> Section:
|
|
400
|
+
"""Create yield analysis section."""
|
|
401
|
+
content_parts = []
|
|
402
|
+
|
|
403
|
+
# Overall yield
|
|
404
|
+
total_duts = len(batch_results)
|
|
405
|
+
passing_duts = sum(
|
|
406
|
+
1 for r in batch_results if r.get("pass_count", 0) == r.get("total_count", 0)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
overall_yield = (passing_duts / total_duts * 100) if total_duts > 0 else 0
|
|
410
|
+
|
|
411
|
+
content_parts.append(f"**Overall Yield:** {overall_yield:.2f}%")
|
|
412
|
+
content_parts.append(f"**Passing DUTs:** {passing_duts}/{total_duts}")
|
|
413
|
+
|
|
414
|
+
# Per-test yield
|
|
415
|
+
content_parts.append("\n**Per-Test Yield:**")
|
|
416
|
+
|
|
417
|
+
# Collect all test names
|
|
418
|
+
all_tests: set[str] = set()
|
|
419
|
+
for result in batch_results:
|
|
420
|
+
if "measurements" in result:
|
|
421
|
+
all_tests.update(result["measurements"].keys())
|
|
422
|
+
|
|
423
|
+
test_yields: list[tuple[str, float, int, int]] = []
|
|
424
|
+
for test_name in sorted(all_tests):
|
|
425
|
+
total_with_test = 0
|
|
426
|
+
passed_test = 0
|
|
427
|
+
|
|
428
|
+
for result in batch_results:
|
|
429
|
+
if "measurements" in result and test_name in result["measurements"]:
|
|
430
|
+
total_with_test += 1
|
|
431
|
+
if result["measurements"][test_name].get("passed", True):
|
|
432
|
+
passed_test += 1
|
|
433
|
+
|
|
434
|
+
if total_with_test > 0:
|
|
435
|
+
test_yield = passed_test / total_with_test * 100
|
|
436
|
+
test_yields.append((test_name, test_yield, passed_test, total_with_test))
|
|
437
|
+
|
|
438
|
+
# Sort by yield (worst first)
|
|
439
|
+
test_yields.sort(key=lambda x: x[1])
|
|
440
|
+
|
|
441
|
+
for test_name, yield_pct, passed, total in test_yields:
|
|
442
|
+
content_parts.append(f"- {test_name}: {yield_pct:.1f}% ({passed}/{total})")
|
|
443
|
+
|
|
444
|
+
content = "\n".join(content_parts)
|
|
445
|
+
|
|
446
|
+
return Section(
|
|
447
|
+
title="Yield Analysis",
|
|
448
|
+
content=content,
|
|
449
|
+
level=1,
|
|
450
|
+
visible=True,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _create_batch_statistics_section(batch_results: list[dict[str, Any]]) -> Section:
|
|
455
|
+
"""Create batch statistical analysis section."""
|
|
456
|
+
from oscura.reporting.formatting import NumberFormatter
|
|
457
|
+
|
|
458
|
+
formatter = NumberFormatter()
|
|
459
|
+
|
|
460
|
+
# Collect measurements across all DUTs
|
|
461
|
+
param_values: dict[str, list[float]] = {}
|
|
462
|
+
|
|
463
|
+
for result in batch_results:
|
|
464
|
+
if "measurements" in result:
|
|
465
|
+
for param, meas in result["measurements"].items():
|
|
466
|
+
value = meas.get("value")
|
|
467
|
+
if value is not None:
|
|
468
|
+
if param not in param_values:
|
|
469
|
+
param_values[param] = []
|
|
470
|
+
param_values[param].append(value)
|
|
471
|
+
|
|
472
|
+
# Build statistics table
|
|
473
|
+
headers = ["Parameter", "Mean", "Std Dev", "Min", "Max", "Range"]
|
|
474
|
+
rows = []
|
|
475
|
+
|
|
476
|
+
for param in sorted(param_values.keys()):
|
|
477
|
+
values = np.array(param_values[param])
|
|
478
|
+
unit = ""
|
|
479
|
+
|
|
480
|
+
# Get unit from first measurement
|
|
481
|
+
for result in batch_results:
|
|
482
|
+
if "measurements" in result and param in result["measurements"]:
|
|
483
|
+
unit = result["measurements"][param].get("unit", "")
|
|
484
|
+
break
|
|
485
|
+
|
|
486
|
+
rows.append(
|
|
487
|
+
[
|
|
488
|
+
param,
|
|
489
|
+
formatter.format(float(np.mean(values)), unit),
|
|
490
|
+
formatter.format(float(np.std(values)), unit),
|
|
491
|
+
formatter.format(float(np.min(values)), unit),
|
|
492
|
+
formatter.format(float(np.max(values)), unit),
|
|
493
|
+
formatter.format(float(np.max(values) - np.min(values)), unit),
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
table = {"type": "table", "headers": headers, "data": rows}
|
|
498
|
+
|
|
499
|
+
return Section(
|
|
500
|
+
title="Batch Statistics",
|
|
501
|
+
content=[table],
|
|
502
|
+
level=1,
|
|
503
|
+
visible=True,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _create_outlier_detection_section(batch_results: list[dict[str, Any]]) -> Section:
|
|
508
|
+
"""Create outlier detection section."""
|
|
509
|
+
content_parts: list[str] = []
|
|
510
|
+
|
|
511
|
+
# Collect measurements
|
|
512
|
+
param_values: dict[str, list[tuple[int, float]]] = {}
|
|
513
|
+
|
|
514
|
+
for i, result in enumerate(batch_results):
|
|
515
|
+
if "measurements" in result:
|
|
516
|
+
for param, meas in result["measurements"].items():
|
|
517
|
+
value = meas.get("value")
|
|
518
|
+
if value is not None:
|
|
519
|
+
if param not in param_values:
|
|
520
|
+
param_values[param] = []
|
|
521
|
+
param_values[param].append((i, value))
|
|
522
|
+
|
|
523
|
+
# Detect outliers using 3-sigma rule
|
|
524
|
+
outliers_found = False
|
|
525
|
+
|
|
526
|
+
for param in sorted(param_values.keys()):
|
|
527
|
+
values_with_idx = param_values[param]
|
|
528
|
+
values = np.array([v for _, v in values_with_idx])
|
|
529
|
+
|
|
530
|
+
mean = float(np.mean(values))
|
|
531
|
+
std = float(np.std(values))
|
|
532
|
+
|
|
533
|
+
if std > 0:
|
|
534
|
+
outlier_indices: list[tuple[int, float, float]] = []
|
|
535
|
+
for idx, value in values_with_idx:
|
|
536
|
+
z_score = abs(value - mean) / std
|
|
537
|
+
if z_score > 3: # 3-sigma rule
|
|
538
|
+
outlier_indices.append((idx, value, z_score))
|
|
539
|
+
|
|
540
|
+
if outlier_indices:
|
|
541
|
+
outliers_found = True
|
|
542
|
+
content_parts.append(f"**{param}:**")
|
|
543
|
+
for idx, value, z_score in outlier_indices:
|
|
544
|
+
dut_id = batch_results[idx].get("dut_id", f"DUT-{idx + 1}")
|
|
545
|
+
content_parts.append(f"- {dut_id}: {value:.3g} (z-score: {z_score:.2f})")
|
|
546
|
+
|
|
547
|
+
if not outliers_found:
|
|
548
|
+
content_parts.append("No statistical outliers detected (3-sigma threshold).")
|
|
549
|
+
|
|
550
|
+
content = "\n".join(content_parts)
|
|
551
|
+
|
|
552
|
+
return Section(
|
|
553
|
+
title="Outlier Detection",
|
|
554
|
+
content=content,
|
|
555
|
+
level=1,
|
|
556
|
+
visible=True,
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _create_dut_section(result: dict[str, Any], index: int) -> Section:
|
|
561
|
+
"""Create individual DUT section."""
|
|
562
|
+
dut_id = result.get("dut_id", f"DUT-{index + 1}")
|
|
563
|
+
|
|
564
|
+
content_parts: list[Any] = []
|
|
565
|
+
|
|
566
|
+
# DUT summary
|
|
567
|
+
pass_count = result.get("pass_count", 0)
|
|
568
|
+
total_count = result.get("total_count", 0)
|
|
569
|
+
|
|
570
|
+
if total_count > 0:
|
|
571
|
+
pass_rate = pass_count / total_count * 100
|
|
572
|
+
content_parts.append(f"Pass rate: {pass_count}/{total_count} ({pass_rate:.1f}%)")
|
|
573
|
+
|
|
574
|
+
# Measurements table
|
|
575
|
+
if "measurements" in result:
|
|
576
|
+
from oscura.reporting.tables import create_measurement_table
|
|
577
|
+
|
|
578
|
+
table = create_measurement_table(result["measurements"], format="dict")
|
|
579
|
+
content_parts.append(table)
|
|
580
|
+
|
|
581
|
+
return Section(
|
|
582
|
+
title=f"DUT: {dut_id}",
|
|
583
|
+
content=content_parts,
|
|
584
|
+
level=2,
|
|
585
|
+
visible=True,
|
|
586
|
+
collapsible=True,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def aggregate_batch_measurements(
|
|
591
|
+
batch_results: list[dict[str, Any]],
|
|
592
|
+
) -> dict[str, NDArray[np.float64]]:
|
|
593
|
+
"""Aggregate measurements across batch for statistical analysis.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
batch_results: List of DUT results.
|
|
597
|
+
|
|
598
|
+
Returns:
|
|
599
|
+
Dictionary mapping parameter name to array of values.
|
|
600
|
+
|
|
601
|
+
References:
|
|
602
|
+
REPORT-009
|
|
603
|
+
"""
|
|
604
|
+
param_values: dict[str, list[float]] = {}
|
|
605
|
+
|
|
606
|
+
for result in batch_results:
|
|
607
|
+
if "measurements" in result:
|
|
608
|
+
for param, meas in result["measurements"].items():
|
|
609
|
+
value = meas.get("value")
|
|
610
|
+
if value is not None:
|
|
611
|
+
if param not in param_values:
|
|
612
|
+
param_values[param] = []
|
|
613
|
+
param_values[param].append(value)
|
|
614
|
+
|
|
615
|
+
return {k: np.array(v) for k, v in param_values.items()}
|