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
oscura/core/lazy.py
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
"""Lazy evaluation module for deferred computation in Oscura workflows.
|
|
2
|
+
|
|
3
|
+
This module provides lazy evaluation primitives that defer computation until
|
|
4
|
+
results are actually accessed. Designed for analysis workflows where not all
|
|
5
|
+
intermediate results may be needed, reducing memory usage and computation time.
|
|
6
|
+
|
|
7
|
+
Key features:
|
|
8
|
+
- Thread-safe lazy evaluation with compute-once semantics
|
|
9
|
+
- Chained operations without eager evaluation
|
|
10
|
+
- Partial evaluation (compute subset of results)
|
|
11
|
+
- Memory-efficient release of source data after computation
|
|
12
|
+
- Integration with Oscura's memory monitoring and progress tracking
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from oscura.core.lazy import LazyResult, lazy, LazyDict
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Defer expensive FFT computation
|
|
19
|
+
>>> @lazy
|
|
20
|
+
>>> def compute_fft(signal, nfft):
|
|
21
|
+
... return np.fft.fft(signal, n=nfft)
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Create lazy result - not computed yet
|
|
24
|
+
>>> lazy_fft = compute_fft(signal, 8192)
|
|
25
|
+
>>> print(lazy_fft.is_computed()) # False
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Access triggers computation
|
|
28
|
+
>>> spectrum = lazy_fft.value # Computed now
|
|
29
|
+
>>> print(lazy_fft.is_computed()) # True
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Multiple accesses use cached result
|
|
32
|
+
>>> spectrum2 = lazy_fft.value # Returns cached value
|
|
33
|
+
>>>
|
|
34
|
+
>>> # LazyDict for multiple lazy results
|
|
35
|
+
>>> results = LazyDict()
|
|
36
|
+
>>> results['fft'] = LazyResult(lambda: np.fft.fft(signal, 8192))
|
|
37
|
+
>>> results['power'] = LazyResult(lambda: np.abs(results['fft'].value)**2)
|
|
38
|
+
>>> # Access triggers computation chain
|
|
39
|
+
>>> power_spectrum = results['power'] # Computes fft, then power
|
|
40
|
+
|
|
41
|
+
References:
|
|
42
|
+
Python lazy evaluation patterns
|
|
43
|
+
Threading locks for thread-safe computation
|
|
44
|
+
Oscura memory monitoring (core.memory_monitor)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import functools
|
|
50
|
+
import threading
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from typing import TYPE_CHECKING, Any
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from collections.abc import Callable
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class LazyComputeStats:
|
|
60
|
+
"""Statistics for lazy computation tracking.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
total_created: Total number of LazyResult instances created.
|
|
64
|
+
total_computed: Number of LazyResult instances that have been computed.
|
|
65
|
+
total_invalidated: Number of invalidations (for delta analysis).
|
|
66
|
+
compute_time_total: Total time spent computing (seconds).
|
|
67
|
+
cache_hits: Number of times a computed result was reused.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> stats = LazyComputeStats()
|
|
71
|
+
>>> stats.total_created += 1
|
|
72
|
+
>>> stats.total_computed += 1
|
|
73
|
+
>>> print(f"Computed: {stats.total_computed}/{stats.total_created}")
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
total_created: int = 0
|
|
77
|
+
total_computed: int = 0
|
|
78
|
+
total_invalidated: int = 0
|
|
79
|
+
compute_time_total: float = 0.0
|
|
80
|
+
cache_hits: int = 0
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def hit_rate(self) -> float:
|
|
84
|
+
"""Calculate cache hit rate.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Fraction of accesses that were cache hits (0.0-1.0).
|
|
88
|
+
"""
|
|
89
|
+
total_accesses = self.total_computed + self.cache_hits
|
|
90
|
+
if total_accesses == 0:
|
|
91
|
+
return 0.0
|
|
92
|
+
return self.cache_hits / total_accesses
|
|
93
|
+
|
|
94
|
+
def __str__(self) -> str:
|
|
95
|
+
"""Format statistics as readable string."""
|
|
96
|
+
return (
|
|
97
|
+
f"Lazy Computation Statistics:\n"
|
|
98
|
+
f" Created: {self.total_created}\n"
|
|
99
|
+
f" Computed: {self.total_computed}\n"
|
|
100
|
+
f" Cache Hits: {self.cache_hits}\n"
|
|
101
|
+
f" Hit Rate: {self.hit_rate * 100:.1f}%\n"
|
|
102
|
+
f" Invalidations: {self.total_invalidated}\n"
|
|
103
|
+
f" Total Compute Time: {self.compute_time_total:.3f}s\n"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Global statistics tracker
|
|
108
|
+
_global_stats = LazyComputeStats()
|
|
109
|
+
_stats_lock = threading.Lock()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_lazy_stats() -> LazyComputeStats:
|
|
113
|
+
"""Get global lazy computation statistics.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Global LazyComputeStats instance.
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
>>> stats = get_lazy_stats()
|
|
120
|
+
>>> print(stats)
|
|
121
|
+
Lazy Computation Statistics:
|
|
122
|
+
Created: 42
|
|
123
|
+
Computed: 35
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
References:
|
|
127
|
+
MEM-031: Cache statistics tracking
|
|
128
|
+
"""
|
|
129
|
+
with _stats_lock:
|
|
130
|
+
return LazyComputeStats(
|
|
131
|
+
total_created=_global_stats.total_created,
|
|
132
|
+
total_computed=_global_stats.total_computed,
|
|
133
|
+
total_invalidated=_global_stats.total_invalidated,
|
|
134
|
+
compute_time_total=_global_stats.compute_time_total,
|
|
135
|
+
cache_hits=_global_stats.cache_hits,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def reset_lazy_stats() -> None:
|
|
140
|
+
"""Reset global lazy computation statistics.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> reset_lazy_stats()
|
|
144
|
+
>>> stats = get_lazy_stats()
|
|
145
|
+
>>> assert stats.total_created == 0
|
|
146
|
+
"""
|
|
147
|
+
global _global_stats
|
|
148
|
+
with _stats_lock:
|
|
149
|
+
_global_stats = LazyComputeStats()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LazyResult[T]:
|
|
153
|
+
"""Deferred computation wrapper with thread-safe compute-once semantics.
|
|
154
|
+
|
|
155
|
+
Wraps a computation function that will be called only when the result is
|
|
156
|
+
first accessed. Subsequent accesses return the cached result. Thread-safe
|
|
157
|
+
for parallel access from multiple analyzers.
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
Attributes:
|
|
161
|
+
name: Optional name for debugging/logging.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
>>> # Create lazy FFT computation
|
|
165
|
+
>>> lazy_fft = LazyResult(
|
|
166
|
+
... lambda: np.fft.fft(signal, n=8192),
|
|
167
|
+
... name="fft_8192"
|
|
168
|
+
... )
|
|
169
|
+
>>>
|
|
170
|
+
>>> # Check if computed without triggering computation
|
|
171
|
+
>>> if not lazy_fft.is_computed():
|
|
172
|
+
... print("Not computed yet")
|
|
173
|
+
>>>
|
|
174
|
+
>>> # Access triggers computation
|
|
175
|
+
>>> spectrum = lazy_fft.value
|
|
176
|
+
>>>
|
|
177
|
+
>>> # Subsequent accesses use cache
|
|
178
|
+
>>> spectrum2 = lazy_fft.value # No recomputation
|
|
179
|
+
>>>
|
|
180
|
+
>>> # Invalidate for delta analysis
|
|
181
|
+
>>> lazy_fft.invalidate()
|
|
182
|
+
>>> spectrum3 = lazy_fft.value # Recomputes
|
|
183
|
+
|
|
184
|
+
References:
|
|
185
|
+
Python threading for thread safety
|
|
186
|
+
PERF-002: Lazy evaluation requirements
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
def __init__(
|
|
190
|
+
self,
|
|
191
|
+
compute_fn: Callable[[], T],
|
|
192
|
+
name: str = "",
|
|
193
|
+
*,
|
|
194
|
+
weak_source: bool = False,
|
|
195
|
+
):
|
|
196
|
+
"""Initialize lazy result.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
compute_fn: Function to call to compute the result.
|
|
200
|
+
name: Optional name for debugging/logging.
|
|
201
|
+
weak_source: If True, use weak reference to source data
|
|
202
|
+
(allows GC after computation).
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> lazy_result = LazyResult(
|
|
206
|
+
... lambda: expensive_computation(),
|
|
207
|
+
... name="expensive_op"
|
|
208
|
+
... )
|
|
209
|
+
"""
|
|
210
|
+
self._compute_fn = compute_fn
|
|
211
|
+
self._name = name or f"LazyResult_{id(self)}"
|
|
212
|
+
self._result: T | None = None
|
|
213
|
+
self._computed = False
|
|
214
|
+
self._lock = threading.RLock()
|
|
215
|
+
self._weak_source = weak_source
|
|
216
|
+
self._source_released = False
|
|
217
|
+
|
|
218
|
+
# Track creation
|
|
219
|
+
with _stats_lock:
|
|
220
|
+
_global_stats.total_created += 1
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def value(self) -> T:
|
|
224
|
+
"""Get the result, computing if necessary.
|
|
225
|
+
|
|
226
|
+
Thread-safe lazy computation with compute-once semantics.
|
|
227
|
+
Multiple concurrent accesses will only compute once.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
The computed result.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
Exception: Any exception raised by the compute function.
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> lazy_fft = LazyResult(lambda: np.fft.fft(signal))
|
|
237
|
+
>>> spectrum = lazy_fft.value # Computes here
|
|
238
|
+
>>> spectrum2 = lazy_fft.value # Uses cache
|
|
239
|
+
|
|
240
|
+
References:
|
|
241
|
+
PERF-002: Lazy evaluation for analysis pipelines
|
|
242
|
+
"""
|
|
243
|
+
with self._lock:
|
|
244
|
+
if self._computed:
|
|
245
|
+
# Cache hit
|
|
246
|
+
with _stats_lock:
|
|
247
|
+
_global_stats.cache_hits += 1
|
|
248
|
+
return self._result # type: ignore[return-value]
|
|
249
|
+
|
|
250
|
+
# Compute result
|
|
251
|
+
import time
|
|
252
|
+
|
|
253
|
+
start_time = time.time()
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
self._result = self._compute_fn()
|
|
257
|
+
self._computed = True
|
|
258
|
+
|
|
259
|
+
# Track computation
|
|
260
|
+
compute_time = time.time() - start_time
|
|
261
|
+
with _stats_lock:
|
|
262
|
+
_global_stats.total_computed += 1
|
|
263
|
+
_global_stats.compute_time_total += compute_time
|
|
264
|
+
|
|
265
|
+
# Optionally release source data
|
|
266
|
+
if self._weak_source and not self._source_released:
|
|
267
|
+
self._release_source()
|
|
268
|
+
|
|
269
|
+
return self._result
|
|
270
|
+
|
|
271
|
+
except Exception:
|
|
272
|
+
# Don't cache errors, allow retry
|
|
273
|
+
self._computed = False
|
|
274
|
+
raise
|
|
275
|
+
|
|
276
|
+
def is_computed(self) -> bool:
|
|
277
|
+
"""Check if result has been computed without triggering computation.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
True if result is computed and cached.
|
|
281
|
+
|
|
282
|
+
Example:
|
|
283
|
+
>>> lazy_result = LazyResult(lambda: expensive_op())
|
|
284
|
+
>>> if lazy_result.is_computed():
|
|
285
|
+
... result = lazy_result.value # No computation
|
|
286
|
+
... else:
|
|
287
|
+
... print("Will compute on next access")
|
|
288
|
+
|
|
289
|
+
References:
|
|
290
|
+
API-012: Lazy result access patterns
|
|
291
|
+
"""
|
|
292
|
+
with self._lock:
|
|
293
|
+
return self._computed
|
|
294
|
+
|
|
295
|
+
def invalidate(self) -> None:
|
|
296
|
+
"""Mark result as invalid, forcing recomputation on next access.
|
|
297
|
+
|
|
298
|
+
Useful for delta analysis where the underlying data changes and
|
|
299
|
+
results need to be recomputed.
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
>>> lazy_fft = LazyResult(lambda: np.fft.fft(signal))
|
|
303
|
+
>>> spectrum1 = lazy_fft.value
|
|
304
|
+
>>>
|
|
305
|
+
>>> # Signal data changed
|
|
306
|
+
>>> signal = new_signal
|
|
307
|
+
>>> lazy_fft.invalidate()
|
|
308
|
+
>>>
|
|
309
|
+
>>> # Next access recomputes with new signal
|
|
310
|
+
>>> spectrum2 = lazy_fft.value
|
|
311
|
+
|
|
312
|
+
References:
|
|
313
|
+
API-012: Delta analysis support
|
|
314
|
+
"""
|
|
315
|
+
with self._lock:
|
|
316
|
+
self._computed = False
|
|
317
|
+
self._result = None
|
|
318
|
+
self._source_released = False
|
|
319
|
+
|
|
320
|
+
with _stats_lock:
|
|
321
|
+
_global_stats.total_invalidated += 1
|
|
322
|
+
|
|
323
|
+
def get_if_computed(self) -> T | None:
|
|
324
|
+
"""Get result only if already computed, otherwise return None.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Computed result or None if not yet computed.
|
|
328
|
+
|
|
329
|
+
Example:
|
|
330
|
+
>>> lazy_result = LazyResult(lambda: expensive_op())
|
|
331
|
+
>>> result = lazy_result.get_if_computed() # None
|
|
332
|
+
>>> _ = lazy_result.value # Compute
|
|
333
|
+
>>> result = lazy_result.get_if_computed() # Returns result
|
|
334
|
+
"""
|
|
335
|
+
with self._lock:
|
|
336
|
+
if self._computed:
|
|
337
|
+
return self._result
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
def peek(self) -> tuple[bool, T | None]:
|
|
341
|
+
"""Get computation status and result if available.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
Tuple of (is_computed, result). Result is None if not computed.
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> lazy_result = LazyResult(lambda: expensive_op())
|
|
348
|
+
>>> computed, result = lazy_result.peek()
|
|
349
|
+
>>> if computed:
|
|
350
|
+
... print(f"Result: {result}")
|
|
351
|
+
... else:
|
|
352
|
+
... print("Not computed yet")
|
|
353
|
+
"""
|
|
354
|
+
with self._lock:
|
|
355
|
+
return (self._computed, self._result)
|
|
356
|
+
|
|
357
|
+
def map(self, fn: Callable[[T], Any]) -> LazyResult[Any]:
|
|
358
|
+
"""Create a new lazy result by applying a function to this result.
|
|
359
|
+
|
|
360
|
+
Enables chained lazy operations without eager evaluation.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
fn: Function to apply to the result.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
New LazyResult that computes fn(self.value).
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> lazy_fft = LazyResult(lambda: np.fft.fft(signal))
|
|
370
|
+
>>> lazy_power = lazy_fft.map(lambda x: np.abs(x)**2)
|
|
371
|
+
>>> lazy_peak = lazy_power.map(lambda x: x.max())
|
|
372
|
+
>>>
|
|
373
|
+
>>> # Nothing computed yet
|
|
374
|
+
>>> peak = lazy_peak.value # Computes entire chain
|
|
375
|
+
|
|
376
|
+
References:
|
|
377
|
+
PERF-002: Lazy evaluation for chained operations
|
|
378
|
+
"""
|
|
379
|
+
return LazyResult(
|
|
380
|
+
lambda: fn(self.value),
|
|
381
|
+
name=f"{self._name}.map({fn.__name__})",
|
|
382
|
+
weak_source=self._weak_source,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _release_source(self) -> None:
|
|
386
|
+
"""Release source data to allow garbage collection.
|
|
387
|
+
|
|
388
|
+
After computation completes, we can release the source data
|
|
389
|
+
if weak_source=True was specified. This replaces the compute
|
|
390
|
+
function's closure to break references to large input data.
|
|
391
|
+
|
|
392
|
+
Note: We don't call gc.collect() here as it would be called
|
|
393
|
+
very frequently and is expensive. Python's automatic GC will
|
|
394
|
+
handle cleanup.
|
|
395
|
+
"""
|
|
396
|
+
# Clear the compute function's closure to release references
|
|
397
|
+
if hasattr(self._compute_fn, "__closure__") and self._compute_fn.__closure__:
|
|
398
|
+
# Can't directly clear closure, but can replace function
|
|
399
|
+
# to break references
|
|
400
|
+
result = self._result
|
|
401
|
+
|
|
402
|
+
def return_result() -> T:
|
|
403
|
+
return result # type: ignore[return-value]
|
|
404
|
+
|
|
405
|
+
self._compute_fn = return_result
|
|
406
|
+
|
|
407
|
+
self._source_released = True
|
|
408
|
+
# Let Python's automatic GC handle cleanup
|
|
409
|
+
|
|
410
|
+
def __repr__(self) -> str:
|
|
411
|
+
"""String representation for debugging."""
|
|
412
|
+
status = "computed" if self._computed else "deferred"
|
|
413
|
+
return f"LazyResult(name={self._name!r}, status={status})"
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class LazyDict(dict[str, Any]):
|
|
417
|
+
"""Dictionary where LazyResult values are auto-evaluated on access.
|
|
418
|
+
|
|
419
|
+
Extends standard dict to automatically evaluate LazyResult values when
|
|
420
|
+
accessed. Regular (non-lazy) values pass through unchanged.
|
|
421
|
+
|
|
422
|
+
Useful for collections of analysis results where some may not be needed.
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
>>> results = LazyDict()
|
|
426
|
+
>>> results['fft'] = LazyResult(lambda: np.fft.fft(signal))
|
|
427
|
+
>>> results['power'] = LazyResult(lambda: np.abs(results['fft'])**2)
|
|
428
|
+
>>> results['constant'] = 42 # Non-lazy value
|
|
429
|
+
>>>
|
|
430
|
+
>>> # Access auto-evaluates lazy results
|
|
431
|
+
>>> fft_spectrum = results['fft'] # Computes FFT
|
|
432
|
+
>>> power_spectrum = results['power'] # Computes power
|
|
433
|
+
>>> const = results['constant'] # Returns 42 directly
|
|
434
|
+
>>>
|
|
435
|
+
>>> # Check if computed without triggering computation
|
|
436
|
+
>>> fft_lazy = super(LazyDict, results).__getitem__('fft')
|
|
437
|
+
>>> if fft_lazy.is_computed():
|
|
438
|
+
... print("FFT already computed")
|
|
439
|
+
|
|
440
|
+
References:
|
|
441
|
+
API-012: Lazy result access patterns
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
def __getitem__(self, key: str) -> Any:
|
|
445
|
+
"""Get value, auto-evaluating if it's a LazyResult.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
key: Dictionary key.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Evaluated value (LazyResult.value) or raw value.
|
|
452
|
+
|
|
453
|
+
Example:
|
|
454
|
+
>>> lazy_dict = LazyDict()
|
|
455
|
+
>>> lazy_dict['result'] = LazyResult(lambda: expensive_op())
|
|
456
|
+
>>> value = lazy_dict['result'] # Auto-evaluates
|
|
457
|
+
"""
|
|
458
|
+
value = super().__getitem__(key)
|
|
459
|
+
if isinstance(value, LazyResult):
|
|
460
|
+
return value.value
|
|
461
|
+
return value
|
|
462
|
+
|
|
463
|
+
def get_lazy(self, key: str) -> LazyResult[Any] | Any:
|
|
464
|
+
"""Get the raw value without auto-evaluation.
|
|
465
|
+
|
|
466
|
+
Returns the LazyResult instance itself, not its value.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
key: Dictionary key.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Raw value (may be LazyResult instance).
|
|
473
|
+
|
|
474
|
+
Example:
|
|
475
|
+
>>> lazy_dict = LazyDict()
|
|
476
|
+
>>> lazy_dict['result'] = LazyResult(lambda: expensive_op())
|
|
477
|
+
>>> lazy_obj = lazy_dict.get_lazy('result')
|
|
478
|
+
>>> if not lazy_obj.is_computed():
|
|
479
|
+
... print("Will compute on access")
|
|
480
|
+
"""
|
|
481
|
+
return super().__getitem__(key)
|
|
482
|
+
|
|
483
|
+
def is_computed(self, key: str) -> bool:
|
|
484
|
+
"""Check if a lazy value has been computed.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
key: Dictionary key.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
True if value is computed (or not lazy), False otherwise.
|
|
491
|
+
|
|
492
|
+
Example:
|
|
493
|
+
>>> if not lazy_dict.is_computed('fft'):
|
|
494
|
+
... print("FFT not computed yet")
|
|
495
|
+
"""
|
|
496
|
+
value = super().__getitem__(key)
|
|
497
|
+
if isinstance(value, LazyResult):
|
|
498
|
+
return value.is_computed()
|
|
499
|
+
return True # Non-lazy values are "computed"
|
|
500
|
+
|
|
501
|
+
def invalidate(self, key: str) -> None:
|
|
502
|
+
"""Invalidate a lazy result, forcing recomputation.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
key: Dictionary key.
|
|
506
|
+
|
|
507
|
+
Example:
|
|
508
|
+
>>> lazy_dict.invalidate('fft')
|
|
509
|
+
>>> # Next access will recompute
|
|
510
|
+
>>> fft = lazy_dict['fft']
|
|
511
|
+
"""
|
|
512
|
+
value = super().__getitem__(key)
|
|
513
|
+
if isinstance(value, LazyResult):
|
|
514
|
+
value.invalidate()
|
|
515
|
+
|
|
516
|
+
def invalidate_all(self) -> None:
|
|
517
|
+
"""Invalidate all lazy results in the dictionary.
|
|
518
|
+
|
|
519
|
+
Example:
|
|
520
|
+
>>> lazy_dict.invalidate_all()
|
|
521
|
+
>>> # All lazy values will recompute on next access
|
|
522
|
+
"""
|
|
523
|
+
for value in self.values():
|
|
524
|
+
if isinstance(value, LazyResult):
|
|
525
|
+
value.invalidate()
|
|
526
|
+
|
|
527
|
+
def computed_keys(self) -> list[str]:
|
|
528
|
+
"""Get list of keys with computed values.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
List of keys whose values are computed.
|
|
532
|
+
|
|
533
|
+
Example:
|
|
534
|
+
>>> computed = lazy_dict.computed_keys()
|
|
535
|
+
>>> print(f"Computed: {computed}")
|
|
536
|
+
"""
|
|
537
|
+
return [
|
|
538
|
+
key
|
|
539
|
+
for key, value in super().items()
|
|
540
|
+
if not isinstance(value, LazyResult) or value.is_computed()
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
def deferred_keys(self) -> list[str]:
|
|
544
|
+
"""Get list of keys with deferred (not computed) values.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
List of keys whose LazyResult values are not computed.
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
>>> deferred = lazy_dict.deferred_keys()
|
|
551
|
+
>>> print(f"Not computed: {deferred}")
|
|
552
|
+
"""
|
|
553
|
+
return [
|
|
554
|
+
key
|
|
555
|
+
for key, value in super().items()
|
|
556
|
+
if isinstance(value, LazyResult) and not value.is_computed()
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def lazy[T](fn: Callable[..., T]) -> Callable[..., LazyResult[T]]:
|
|
561
|
+
"""Decorator to make a function return a LazyResult.
|
|
562
|
+
|
|
563
|
+
Wraps a function so it returns a LazyResult instead of computing
|
|
564
|
+
immediately. Useful for expensive analysis functions.
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
fn: Function to wrap.
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
Wrapped function that returns LazyResult.
|
|
571
|
+
|
|
572
|
+
Example:
|
|
573
|
+
>>> @lazy
|
|
574
|
+
... def compute_fft(signal, nfft):
|
|
575
|
+
... print("Computing FFT...")
|
|
576
|
+
... return np.fft.fft(signal, n=nfft)
|
|
577
|
+
>>>
|
|
578
|
+
>>> # Returns LazyResult, doesn't compute yet
|
|
579
|
+
>>> lazy_fft = compute_fft(signal, 8192)
|
|
580
|
+
>>> print("Created lazy result")
|
|
581
|
+
>>>
|
|
582
|
+
>>> # Access triggers computation
|
|
583
|
+
>>> spectrum = lazy_fft.value
|
|
584
|
+
>>> # Prints: "Computing FFT..."
|
|
585
|
+
>>>
|
|
586
|
+
>>> # Second access uses cache
|
|
587
|
+
>>> spectrum2 = lazy_fft.value
|
|
588
|
+
>>> # No print - uses cached result
|
|
589
|
+
|
|
590
|
+
References:
|
|
591
|
+
PERF-002: Lazy evaluation for analysis pipelines
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
@functools.wraps(fn)
|
|
595
|
+
def wrapper(*args: Any, **kwargs: Any) -> LazyResult[T]:
|
|
596
|
+
def compute() -> T:
|
|
597
|
+
return fn(*args, **kwargs)
|
|
598
|
+
|
|
599
|
+
return LazyResult(
|
|
600
|
+
compute,
|
|
601
|
+
name=fn.__name__,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
return wrapper
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class LazyAnalysisResult:
|
|
608
|
+
"""Lazy wrapper for multi-domain analysis results.
|
|
609
|
+
|
|
610
|
+
Provides lazy evaluation for analysis engines that produce results across
|
|
611
|
+
multiple domains (time, frequency, statistical, etc.). Only computes
|
|
612
|
+
results for domains that are actually accessed.
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
Attributes:
|
|
616
|
+
domains: List of available analysis domains.
|
|
617
|
+
|
|
618
|
+
Example:
|
|
619
|
+
>>> # Create analyzer with multiple domains
|
|
620
|
+
>>> analyzer = SignalAnalyzer()
|
|
621
|
+
>>>
|
|
622
|
+
>>> # Wrap in lazy result - nothing computed yet
|
|
623
|
+
>>> lazy_results = LazyAnalysisResult(
|
|
624
|
+
... analyzer,
|
|
625
|
+
... signal_data,
|
|
626
|
+
... domains=['time', 'frequency', 'statistics']
|
|
627
|
+
... )
|
|
628
|
+
>>>
|
|
629
|
+
>>> # Only compute frequency domain
|
|
630
|
+
>>> freq_results = lazy_results.get_domain('frequency')
|
|
631
|
+
>>> # Time and statistics domains not computed
|
|
632
|
+
>>>
|
|
633
|
+
>>> # Check what's been computed
|
|
634
|
+
>>> print(lazy_results.computed_domains()) # ['frequency']
|
|
635
|
+
>>> print(lazy_results.deferred_domains()) # ['time', 'statistics']
|
|
636
|
+
>>>
|
|
637
|
+
>>> # Access multiple domains
|
|
638
|
+
>>> all_results = lazy_results.compute_all()
|
|
639
|
+
|
|
640
|
+
References:
|
|
641
|
+
PERF-002: Lazy evaluation requirements
|
|
642
|
+
API-012: Multi-domain analysis patterns
|
|
643
|
+
"""
|
|
644
|
+
|
|
645
|
+
def __init__(
|
|
646
|
+
self,
|
|
647
|
+
engine: Any,
|
|
648
|
+
data: Any,
|
|
649
|
+
domains: list[str],
|
|
650
|
+
*,
|
|
651
|
+
compute_fn_template: Callable[[Any, Any, str], Any] | None = None,
|
|
652
|
+
):
|
|
653
|
+
"""Initialize lazy analysis result.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
engine: Analysis engine instance.
|
|
657
|
+
data: Input data for analysis.
|
|
658
|
+
domains: List of available analysis domains.
|
|
659
|
+
compute_fn_template: Optional custom compute function.
|
|
660
|
+
Signature: fn(engine, data, domain) -> result.
|
|
661
|
+
Default uses engine.analyze(data, domain=domain).
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> lazy_results = LazyAnalysisResult(
|
|
665
|
+
... my_analyzer,
|
|
666
|
+
... signal_data,
|
|
667
|
+
... domains=['time', 'frequency', 'wavelet']
|
|
668
|
+
... )
|
|
669
|
+
"""
|
|
670
|
+
self._engine = engine
|
|
671
|
+
self._data = data
|
|
672
|
+
self.domains = domains
|
|
673
|
+
self._compute_fn = compute_fn_template or self._default_compute
|
|
674
|
+
|
|
675
|
+
# Create lazy results for each domain
|
|
676
|
+
self._domain_results = LazyDict()
|
|
677
|
+
for domain in domains:
|
|
678
|
+
|
|
679
|
+
def make_compute_fn(d: str = domain) -> Callable[[], Any]:
|
|
680
|
+
def compute_domain() -> Any:
|
|
681
|
+
return self._compute_fn(self._engine, self._data, d)
|
|
682
|
+
|
|
683
|
+
return compute_domain
|
|
684
|
+
|
|
685
|
+
self._domain_results[domain] = LazyResult(
|
|
686
|
+
make_compute_fn(),
|
|
687
|
+
name=f"{engine.__class__.__name__}.{domain}",
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
def _default_compute(self, engine: Any, data: Any, domain: str) -> dict[str, Any]:
|
|
691
|
+
"""Default compute function for domain analysis.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
engine: Analysis engine.
|
|
695
|
+
data: Input data.
|
|
696
|
+
domain: Domain to analyze.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Analysis result for domain.
|
|
700
|
+
|
|
701
|
+
Raises:
|
|
702
|
+
AttributeError: If engine has no analyze() or analyze_{domain}() method.
|
|
703
|
+
"""
|
|
704
|
+
# Try common patterns
|
|
705
|
+
if hasattr(engine, "analyze"):
|
|
706
|
+
result: dict[str, Any] = engine.analyze(data, domain=domain)
|
|
707
|
+
return result
|
|
708
|
+
elif hasattr(engine, f"analyze_{domain}"):
|
|
709
|
+
method = getattr(engine, f"analyze_{domain}")
|
|
710
|
+
result = method(data)
|
|
711
|
+
return result
|
|
712
|
+
else:
|
|
713
|
+
raise AttributeError(
|
|
714
|
+
f"Engine {engine.__class__.__name__} has no analyze() method "
|
|
715
|
+
f"or analyze_{domain}() method"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def get_domain(self, domain: str) -> Any:
|
|
719
|
+
"""Get results for specific domain, computing only that domain.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
domain: Domain name (e.g., 'time', 'frequency').
|
|
723
|
+
|
|
724
|
+
Returns:
|
|
725
|
+
Analysis results for the domain.
|
|
726
|
+
|
|
727
|
+
Raises:
|
|
728
|
+
KeyError: If domain not available.
|
|
729
|
+
|
|
730
|
+
Example:
|
|
731
|
+
>>> freq_results = lazy_results.get_domain('frequency')
|
|
732
|
+
>>> # Only frequency domain computed
|
|
733
|
+
|
|
734
|
+
References:
|
|
735
|
+
PERF-002: Partial evaluation
|
|
736
|
+
"""
|
|
737
|
+
if domain not in self.domains:
|
|
738
|
+
raise KeyError(f"Domain '{domain}' not available. Available: {self.domains}")
|
|
739
|
+
return self._domain_results[domain]
|
|
740
|
+
|
|
741
|
+
def computed_domains(self) -> list[str]:
|
|
742
|
+
"""Get list of domains that have been computed.
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
List of computed domain names.
|
|
746
|
+
|
|
747
|
+
Example:
|
|
748
|
+
>>> computed = lazy_results.computed_domains()
|
|
749
|
+
>>> print(f"Computed: {computed}")
|
|
750
|
+
"""
|
|
751
|
+
return self._domain_results.computed_keys()
|
|
752
|
+
|
|
753
|
+
def deferred_domains(self) -> list[str]:
|
|
754
|
+
"""Get list of domains that have not been computed.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
List of deferred domain names.
|
|
758
|
+
|
|
759
|
+
Example:
|
|
760
|
+
>>> deferred = lazy_results.deferred_domains()
|
|
761
|
+
>>> print(f"Not computed: {deferred}")
|
|
762
|
+
"""
|
|
763
|
+
return self._domain_results.deferred_keys()
|
|
764
|
+
|
|
765
|
+
def compute_all(self) -> dict[str, Any]:
|
|
766
|
+
"""Compute all domains and return results dictionary.
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
Dictionary mapping domain names to results.
|
|
770
|
+
|
|
771
|
+
Example:
|
|
772
|
+
>>> all_results = lazy_results.compute_all()
|
|
773
|
+
>>> print(all_results.keys()) # All domains
|
|
774
|
+
|
|
775
|
+
References:
|
|
776
|
+
API-012: Bulk computation
|
|
777
|
+
"""
|
|
778
|
+
return {domain: self.get_domain(domain) for domain in self.domains}
|
|
779
|
+
|
|
780
|
+
def invalidate_domain(self, domain: str) -> None:
|
|
781
|
+
"""Invalidate a specific domain's results.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
domain: Domain to invalidate.
|
|
785
|
+
|
|
786
|
+
Example:
|
|
787
|
+
>>> lazy_results.invalidate_domain('frequency')
|
|
788
|
+
>>> # Next access will recompute
|
|
789
|
+
"""
|
|
790
|
+
self._domain_results.invalidate(domain)
|
|
791
|
+
|
|
792
|
+
def invalidate_all(self) -> None:
|
|
793
|
+
"""Invalidate all domain results.
|
|
794
|
+
|
|
795
|
+
Example:
|
|
796
|
+
>>> lazy_results.invalidate_all()
|
|
797
|
+
>>> # All domains will recompute on next access
|
|
798
|
+
"""
|
|
799
|
+
self._domain_results.invalidate_all()
|
|
800
|
+
|
|
801
|
+
def __getitem__(self, domain: str) -> Any:
|
|
802
|
+
"""Dictionary-style access to domains.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
domain: Domain name.
|
|
806
|
+
|
|
807
|
+
Returns:
|
|
808
|
+
Domain results.
|
|
809
|
+
|
|
810
|
+
Example:
|
|
811
|
+
>>> freq_results = lazy_results['frequency']
|
|
812
|
+
"""
|
|
813
|
+
return self.get_domain(domain)
|
|
814
|
+
|
|
815
|
+
def __repr__(self) -> str:
|
|
816
|
+
"""String representation for debugging."""
|
|
817
|
+
computed = self.computed_domains()
|
|
818
|
+
deferred = self.deferred_domains()
|
|
819
|
+
return (
|
|
820
|
+
f"LazyAnalysisResult(domains={self.domains}, computed={computed}, deferred={deferred})"
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
__all__ = [
|
|
825
|
+
"LazyAnalysisResult",
|
|
826
|
+
"LazyComputeStats",
|
|
827
|
+
"LazyDict",
|
|
828
|
+
"LazyResult",
|
|
829
|
+
"get_lazy_stats",
|
|
830
|
+
"lazy",
|
|
831
|
+
"reset_lazy_stats",
|
|
832
|
+
]
|