oscura 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Number and value formatting for Oscura reports.
|
|
2
|
+
|
|
3
|
+
This module provides smart number formatting with SI prefixes,
|
|
4
|
+
significant figures, and contextual annotations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.reporting import format_with_units, format_with_context
|
|
9
|
+
>>> format_with_units(0.0000023, "s") # "2.3 us"
|
|
10
|
+
>>> format_with_context(2.3e-9, spec=5e-9) # "2.3 ns (spec <5 ns, PASS)"
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import locale as locale_module
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import Literal
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
# SI prefixes
|
|
23
|
+
SI_PREFIXES = {
|
|
24
|
+
24: "Y",
|
|
25
|
+
21: "Z",
|
|
26
|
+
18: "E",
|
|
27
|
+
15: "P",
|
|
28
|
+
12: "T",
|
|
29
|
+
9: "G",
|
|
30
|
+
6: "M",
|
|
31
|
+
3: "k",
|
|
32
|
+
0: "",
|
|
33
|
+
-3: "m",
|
|
34
|
+
-6: "u",
|
|
35
|
+
-9: "n",
|
|
36
|
+
-12: "p",
|
|
37
|
+
-15: "f",
|
|
38
|
+
-18: "a",
|
|
39
|
+
-21: "z",
|
|
40
|
+
-24: "y",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Unicode SI prefixes
|
|
44
|
+
SI_PREFIXES_UNICODE = {
|
|
45
|
+
**SI_PREFIXES,
|
|
46
|
+
-6: "\u03bc", # Greek mu
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class NumberFormatter:
|
|
52
|
+
"""Configurable number formatter.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
sig_figs: Significant figures (default 3).
|
|
56
|
+
auto_scale: Use SI prefixes for scaling.
|
|
57
|
+
engineering_notation: Use engineering notation (10^3, 10^6, etc.).
|
|
58
|
+
unicode_prefixes: Use Unicode characters (e.g., micro symbol).
|
|
59
|
+
min_exp: Minimum exponent before using scientific notation.
|
|
60
|
+
max_exp: Maximum exponent before using scientific notation.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
sig_figs: int = 3
|
|
64
|
+
auto_scale: bool = True
|
|
65
|
+
engineering_notation: bool = True
|
|
66
|
+
unicode_prefixes: bool = True
|
|
67
|
+
min_exp: int = -3
|
|
68
|
+
max_exp: int = 3
|
|
69
|
+
|
|
70
|
+
def format(
|
|
71
|
+
self,
|
|
72
|
+
value: float,
|
|
73
|
+
unit: str = "",
|
|
74
|
+
*,
|
|
75
|
+
decimal_places: int | None = None,
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Format a numeric value.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
value: Value to format.
|
|
81
|
+
unit: Unit suffix (e.g., "s", "Hz", "V").
|
|
82
|
+
decimal_places: Override significant figures with fixed decimals.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Formatted string.
|
|
86
|
+
|
|
87
|
+
Example:
|
|
88
|
+
>>> fmt = NumberFormatter()
|
|
89
|
+
>>> fmt.format(0.0000023, "s")
|
|
90
|
+
'2.30 us'
|
|
91
|
+
"""
|
|
92
|
+
if not np.isfinite(value):
|
|
93
|
+
if np.isnan(value):
|
|
94
|
+
return "NaN"
|
|
95
|
+
elif value > 0:
|
|
96
|
+
return "+Inf"
|
|
97
|
+
else:
|
|
98
|
+
return "-Inf"
|
|
99
|
+
|
|
100
|
+
if value == 0:
|
|
101
|
+
return f"0 {unit}".strip()
|
|
102
|
+
|
|
103
|
+
if self.auto_scale and self.engineering_notation:
|
|
104
|
+
return self._format_engineering(value, unit, decimal_places)
|
|
105
|
+
elif self.auto_scale:
|
|
106
|
+
return self._format_scaled(value, unit, decimal_places)
|
|
107
|
+
else:
|
|
108
|
+
return self._format_plain(value, unit, decimal_places)
|
|
109
|
+
|
|
110
|
+
def _format_engineering(
|
|
111
|
+
self,
|
|
112
|
+
value: float,
|
|
113
|
+
unit: str,
|
|
114
|
+
decimal_places: int | None,
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Format with engineering notation (SI prefixes)."""
|
|
117
|
+
abs_value = abs(value)
|
|
118
|
+
sign = "-" if value < 0 else ""
|
|
119
|
+
|
|
120
|
+
# Find appropriate SI prefix
|
|
121
|
+
if abs_value == 0:
|
|
122
|
+
exp = 0
|
|
123
|
+
else:
|
|
124
|
+
exp = int(np.floor(np.log10(abs_value)))
|
|
125
|
+
# Round to nearest multiple of 3
|
|
126
|
+
exp = (exp // 3) * 3
|
|
127
|
+
|
|
128
|
+
# Clamp to available prefixes
|
|
129
|
+
exp = max(-24, min(24, exp))
|
|
130
|
+
|
|
131
|
+
# Get prefix
|
|
132
|
+
prefixes = SI_PREFIXES_UNICODE if self.unicode_prefixes else SI_PREFIXES
|
|
133
|
+
prefix = prefixes.get(exp, "")
|
|
134
|
+
|
|
135
|
+
# Scale value
|
|
136
|
+
scaled = abs_value / (10**exp)
|
|
137
|
+
|
|
138
|
+
# Format with significant figures
|
|
139
|
+
if decimal_places is not None:
|
|
140
|
+
formatted = f"{sign}{scaled:.{decimal_places}f}"
|
|
141
|
+
else:
|
|
142
|
+
# Calculate decimal places from significant figures
|
|
143
|
+
if scaled >= 100:
|
|
144
|
+
decimals = max(0, self.sig_figs - 3)
|
|
145
|
+
elif scaled >= 10:
|
|
146
|
+
decimals = max(0, self.sig_figs - 2)
|
|
147
|
+
elif scaled >= 1:
|
|
148
|
+
decimals = max(0, self.sig_figs - 1)
|
|
149
|
+
else:
|
|
150
|
+
decimals = self.sig_figs
|
|
151
|
+
formatted = f"{sign}{scaled:.{decimals}f}"
|
|
152
|
+
|
|
153
|
+
return f"{formatted} {prefix}{unit}".strip()
|
|
154
|
+
|
|
155
|
+
def _format_scaled(
|
|
156
|
+
self,
|
|
157
|
+
value: float,
|
|
158
|
+
unit: str,
|
|
159
|
+
decimal_places: int | None,
|
|
160
|
+
) -> str:
|
|
161
|
+
"""Format with auto-scaling but not necessarily engineering notation."""
|
|
162
|
+
return self._format_engineering(value, unit, decimal_places)
|
|
163
|
+
|
|
164
|
+
def _format_plain(
|
|
165
|
+
self,
|
|
166
|
+
value: float,
|
|
167
|
+
unit: str,
|
|
168
|
+
decimal_places: int | None,
|
|
169
|
+
) -> str:
|
|
170
|
+
"""Format without scaling."""
|
|
171
|
+
if decimal_places is not None:
|
|
172
|
+
formatted = f"{value:.{decimal_places}f}"
|
|
173
|
+
else:
|
|
174
|
+
formatted = f"{value:.{self.sig_figs}g}"
|
|
175
|
+
|
|
176
|
+
return f"{formatted} {unit}".strip()
|
|
177
|
+
|
|
178
|
+
def format_percentage(self, value: float, *, decimals: int = 1) -> str:
|
|
179
|
+
"""Format as percentage.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
value: Value (0-1 or 0-100).
|
|
183
|
+
decimals: Decimal places.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Percentage string.
|
|
187
|
+
"""
|
|
188
|
+
# Assume value is already in percent if > 1
|
|
189
|
+
if abs(value) <= 1:
|
|
190
|
+
value = value * 100
|
|
191
|
+
return f"{value:.{decimals}f}%"
|
|
192
|
+
|
|
193
|
+
def format_range(
|
|
194
|
+
self,
|
|
195
|
+
min_val: float,
|
|
196
|
+
typ_val: float,
|
|
197
|
+
max_val: float,
|
|
198
|
+
unit: str = "",
|
|
199
|
+
) -> str:
|
|
200
|
+
"""Format min/typ/max range.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
min_val: Minimum value.
|
|
204
|
+
typ_val: Typical value.
|
|
205
|
+
max_val: Maximum value.
|
|
206
|
+
unit: Unit suffix.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Formatted range string.
|
|
210
|
+
"""
|
|
211
|
+
# Use same scaling for all values
|
|
212
|
+
abs_max = max(abs(min_val), abs(typ_val), abs(max_val))
|
|
213
|
+
exp = 0 if abs_max == 0 else int(np.floor(np.log10(abs_max))) // 3 * 3
|
|
214
|
+
exp = max(-24, min(24, exp))
|
|
215
|
+
|
|
216
|
+
prefixes = SI_PREFIXES_UNICODE if self.unicode_prefixes else SI_PREFIXES
|
|
217
|
+
prefix = prefixes.get(exp, "")
|
|
218
|
+
|
|
219
|
+
scale = 10**exp
|
|
220
|
+
decimals = max(0, self.sig_figs - 1)
|
|
221
|
+
|
|
222
|
+
min_s = f"{min_val / scale:.{decimals}f}"
|
|
223
|
+
typ_s = f"{typ_val / scale:.{decimals}f}"
|
|
224
|
+
max_s = f"{max_val / scale:.{decimals}f}"
|
|
225
|
+
|
|
226
|
+
return f"min/typ/max: {min_s} / {typ_s} / {max_s} {prefix}{unit}".strip()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# Default formatter
|
|
230
|
+
_default_formatter = NumberFormatter()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def format_value(
|
|
234
|
+
value: float,
|
|
235
|
+
unit: str = "",
|
|
236
|
+
*,
|
|
237
|
+
sig_figs: int = 3,
|
|
238
|
+
) -> str:
|
|
239
|
+
"""Format a numeric value with SI prefix.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
value: Value to format.
|
|
243
|
+
unit: Unit suffix.
|
|
244
|
+
sig_figs: Significant figures.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Formatted string.
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> format_value(0.0000023, "s")
|
|
251
|
+
'2.30 us'
|
|
252
|
+
"""
|
|
253
|
+
formatter = NumberFormatter(sig_figs=sig_figs)
|
|
254
|
+
return formatter.format(value, unit)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def format_with_units(
|
|
258
|
+
value: float,
|
|
259
|
+
unit: str,
|
|
260
|
+
*,
|
|
261
|
+
sig_figs: int = 3,
|
|
262
|
+
) -> str:
|
|
263
|
+
"""Format value with automatic SI prefix scaling.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
value: Value to format.
|
|
267
|
+
unit: Base unit (e.g., "s", "Hz", "V").
|
|
268
|
+
sig_figs: Significant figures.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Formatted string with SI prefix.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> format_with_units(2300000, "Hz")
|
|
275
|
+
'2.30 MHz'
|
|
276
|
+
"""
|
|
277
|
+
return format_value(value, unit, sig_figs=sig_figs)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def format_with_context(
|
|
281
|
+
value: float,
|
|
282
|
+
*,
|
|
283
|
+
spec: float | None = None,
|
|
284
|
+
spec_type: Literal["max", "min", "exact"] = "max",
|
|
285
|
+
unit: str = "",
|
|
286
|
+
sig_figs: int = 3,
|
|
287
|
+
show_margin: bool = True,
|
|
288
|
+
) -> str:
|
|
289
|
+
"""Format value with specification context.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
value: Measured value.
|
|
293
|
+
spec: Specification limit.
|
|
294
|
+
spec_type: Type of specification (max, min, exact).
|
|
295
|
+
unit: Unit suffix.
|
|
296
|
+
sig_figs: Significant figures.
|
|
297
|
+
show_margin: Show margin percentage.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Formatted string with context.
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> format_with_context(2.3e-9, spec=5e-9, unit="s")
|
|
304
|
+
'2.30 ns (spec <5.00 ns, PASS 54%)'
|
|
305
|
+
"""
|
|
306
|
+
formatter = NumberFormatter(sig_figs=sig_figs)
|
|
307
|
+
value_str = formatter.format(value, unit)
|
|
308
|
+
|
|
309
|
+
if spec is None:
|
|
310
|
+
return value_str
|
|
311
|
+
|
|
312
|
+
spec_str = formatter.format(spec, unit)
|
|
313
|
+
|
|
314
|
+
# Determine pass/fail
|
|
315
|
+
if spec_type == "max":
|
|
316
|
+
passed = value <= spec
|
|
317
|
+
spec_prefix = "<"
|
|
318
|
+
elif spec_type == "min":
|
|
319
|
+
passed = value >= spec
|
|
320
|
+
spec_prefix = ">"
|
|
321
|
+
else: # exact
|
|
322
|
+
passed = abs(value - spec) < spec * 0.01 # 1% tolerance
|
|
323
|
+
spec_prefix = "="
|
|
324
|
+
|
|
325
|
+
status_char = "\u2713" if passed else "\u2717" # Check/X marks
|
|
326
|
+
|
|
327
|
+
# Calculate margin
|
|
328
|
+
margin_str = ""
|
|
329
|
+
if show_margin and spec != 0:
|
|
330
|
+
if spec_type == "max":
|
|
331
|
+
margin = (spec - value) / spec * 100
|
|
332
|
+
elif spec_type == "min":
|
|
333
|
+
margin = (value - spec) / spec * 100
|
|
334
|
+
else:
|
|
335
|
+
margin = (1 - abs(value - spec) / spec) * 100
|
|
336
|
+
|
|
337
|
+
margin_str = f" {margin:.0f}%"
|
|
338
|
+
|
|
339
|
+
return f"{value_str} (spec {spec_prefix}{spec_str}, {status_char}{margin_str})"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def format_pass_fail(passed: bool, *, with_symbol: bool = True) -> str:
|
|
343
|
+
"""Format pass/fail status.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
passed: True for pass, False for fail.
|
|
347
|
+
with_symbol: Include Unicode symbol.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Formatted status string.
|
|
351
|
+
"""
|
|
352
|
+
if with_symbol:
|
|
353
|
+
if passed:
|
|
354
|
+
return "\u2713 PASS" # Check mark
|
|
355
|
+
else:
|
|
356
|
+
return "\u2717 FAIL" # X mark
|
|
357
|
+
else:
|
|
358
|
+
return "PASS" if passed else "FAIL"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def format_margin(
|
|
362
|
+
value: float,
|
|
363
|
+
limit: float,
|
|
364
|
+
*,
|
|
365
|
+
limit_type: Literal["upper", "lower"] = "upper",
|
|
366
|
+
) -> str:
|
|
367
|
+
"""Format margin to limit.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
value: Measured value.
|
|
371
|
+
limit: Limit value.
|
|
372
|
+
limit_type: Whether limit is upper or lower bound.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Margin string with status indicator.
|
|
376
|
+
"""
|
|
377
|
+
if limit_type == "upper":
|
|
378
|
+
margin = limit - value
|
|
379
|
+
margin_pct = (margin / limit * 100) if limit != 0 else 0
|
|
380
|
+
else:
|
|
381
|
+
margin = value - limit
|
|
382
|
+
margin_pct = (margin / limit * 100) if limit != 0 else 0
|
|
383
|
+
|
|
384
|
+
# Status based on margin
|
|
385
|
+
if margin_pct > 20:
|
|
386
|
+
status = "good"
|
|
387
|
+
elif margin_pct > 10:
|
|
388
|
+
status = "ok"
|
|
389
|
+
elif margin_pct > 0:
|
|
390
|
+
status = "marginal"
|
|
391
|
+
else:
|
|
392
|
+
status = "violation"
|
|
393
|
+
|
|
394
|
+
return f"margin: {margin_pct:.1f}% ({status})"
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def format_with_locale(
|
|
398
|
+
value: float | None = None,
|
|
399
|
+
locale: str | None = None,
|
|
400
|
+
*,
|
|
401
|
+
date_value: float | None = None,
|
|
402
|
+
) -> str:
|
|
403
|
+
"""Format numbers/dates with locale-aware formatting.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
value: Numeric value to format (mutually exclusive with date_value).
|
|
407
|
+
locale: Locale string (e.g., 'en_US', 'de_DE', 'fr_FR').
|
|
408
|
+
If None, uses system locale.
|
|
409
|
+
date_value: Timestamp to format as date (mutually exclusive with value).
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Formatted string with locale-specific separators and formats.
|
|
413
|
+
|
|
414
|
+
Example:
|
|
415
|
+
>>> format_with_locale(1234.56, locale="en_US")
|
|
416
|
+
'1,234.56'
|
|
417
|
+
>>> format_with_locale(1234.56, locale="de_DE")
|
|
418
|
+
'1.234,56'
|
|
419
|
+
>>> format_with_locale(1234.56, locale="fr_FR")
|
|
420
|
+
'1 234,56'
|
|
421
|
+
|
|
422
|
+
References:
|
|
423
|
+
REPORT-026: Locale-aware Formatting
|
|
424
|
+
"""
|
|
425
|
+
# Determine locale
|
|
426
|
+
current_locale = locale_module.getlocale()[0] or "en_US" if locale is None else locale
|
|
427
|
+
|
|
428
|
+
# Format date if date_value provided
|
|
429
|
+
if date_value is not None:
|
|
430
|
+
dt = datetime.fromtimestamp(date_value)
|
|
431
|
+
if current_locale.startswith("en_US"):
|
|
432
|
+
return dt.strftime("%m/%d/%Y")
|
|
433
|
+
elif current_locale.startswith("de_DE"):
|
|
434
|
+
return dt.strftime("%d.%m.%Y")
|
|
435
|
+
elif current_locale.startswith("fr_FR"):
|
|
436
|
+
return dt.strftime("%d/%m/%Y")
|
|
437
|
+
else: # ISO format as fallback
|
|
438
|
+
return dt.strftime("%Y-%m-%d")
|
|
439
|
+
|
|
440
|
+
# Format number
|
|
441
|
+
if value is None:
|
|
442
|
+
return ""
|
|
443
|
+
|
|
444
|
+
# Locale-specific decimal and thousands separators
|
|
445
|
+
if current_locale.startswith("en_US"):
|
|
446
|
+
decimal_sep = "."
|
|
447
|
+
thousands_sep = ","
|
|
448
|
+
elif current_locale.startswith("de_DE"):
|
|
449
|
+
decimal_sep = ","
|
|
450
|
+
thousands_sep = "."
|
|
451
|
+
elif current_locale.startswith("fr_FR"):
|
|
452
|
+
decimal_sep = ","
|
|
453
|
+
thousands_sep = " "
|
|
454
|
+
else: # SI standard (space for thousands)
|
|
455
|
+
decimal_sep = "."
|
|
456
|
+
thousands_sep = " "
|
|
457
|
+
|
|
458
|
+
# Format with 2 decimal places
|
|
459
|
+
formatted = f"{value:,.2f}"
|
|
460
|
+
|
|
461
|
+
# Replace separators
|
|
462
|
+
formatted = formatted.replace(",", "TEMP")
|
|
463
|
+
formatted = formatted.replace(".", decimal_sep)
|
|
464
|
+
formatted = formatted.replace("TEMP", thousands_sep)
|
|
465
|
+
|
|
466
|
+
return formatted
|