oscura 0.8.0__py3-none-any.whl → 0.11.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 +19 -19
- oscura/__main__.py +4 -0
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/ml/signal_classifier.py +6 -0
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +182 -84
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/validation.py +17 -10
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/sessions/legacy.py +49 -1
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +12 -9
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- oscura-0.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- oscura-0.8.0.dist-info/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/sessions/legacy.py
CHANGED
|
@@ -17,6 +17,7 @@ import gzip
|
|
|
17
17
|
import hashlib
|
|
18
18
|
import hmac
|
|
19
19
|
import pickle
|
|
20
|
+
import secrets
|
|
20
21
|
from dataclasses import dataclass, field
|
|
21
22
|
from datetime import datetime
|
|
22
23
|
from enum import Enum
|
|
@@ -28,7 +29,45 @@ from oscura.core.exceptions import SecurityError
|
|
|
28
29
|
# Session file format constants
|
|
29
30
|
_SESSION_MAGIC = b"OSC1" # Magic bytes for new format with signature
|
|
30
31
|
_SESSION_SIGNATURE_SIZE = 32 # SHA256 hash size in bytes
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_security_key() -> bytes:
|
|
35
|
+
"""Get or generate per-installation session security key.
|
|
36
|
+
|
|
37
|
+
The key is generated once per installation and stored in ~/.oscura/session_key
|
|
38
|
+
with restrictive permissions (0o600). This provides better security than a
|
|
39
|
+
shared hardcoded key.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
32-byte security key for HMAC signing.
|
|
43
|
+
"""
|
|
44
|
+
key_file = Path.home() / ".oscura" / "session_key"
|
|
45
|
+
|
|
46
|
+
if key_file.exists():
|
|
47
|
+
# Load existing key
|
|
48
|
+
try:
|
|
49
|
+
return key_file.read_bytes()
|
|
50
|
+
except (OSError, PermissionError):
|
|
51
|
+
# Fall back to generating new key if can't read
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
# Generate new random key
|
|
55
|
+
key_file.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
key = secrets.token_bytes(32)
|
|
57
|
+
|
|
58
|
+
# Write with restrictive permissions
|
|
59
|
+
try:
|
|
60
|
+
key_file.write_bytes(key)
|
|
61
|
+
key_file.chmod(0o600) # Owner read/write only
|
|
62
|
+
except (OSError, PermissionError):
|
|
63
|
+
# Can't write key file - continue with ephemeral key
|
|
64
|
+
# This happens in read-only filesystems or restricted environments
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
return key
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_SECURITY_KEY = _get_security_key()
|
|
32
71
|
|
|
33
72
|
|
|
34
73
|
class AnnotationType(Enum):
|
|
@@ -709,6 +748,15 @@ class Session:
|
|
|
709
748
|
def load_session(path: str | Path) -> Session:
|
|
710
749
|
"""Load session from file.
|
|
711
750
|
|
|
751
|
+
This function implements HMAC-SHA256 signature verification before deserializing
|
|
752
|
+
session data to protect against tampering and malicious file modifications.
|
|
753
|
+
|
|
754
|
+
Security:
|
|
755
|
+
Session files are protected with HMAC-SHA256 signatures. Only load session
|
|
756
|
+
files from trusted sources. While HMAC verification prevents tampering,
|
|
757
|
+
the shared security key means all installations can verify each other's
|
|
758
|
+
files. Consider using per-installation keys for sensitive deployments.
|
|
759
|
+
|
|
712
760
|
Args:
|
|
713
761
|
path: Path to session file (.tks).
|
|
714
762
|
|
oscura/side_channel/__init__.py
CHANGED
|
@@ -1,63 +1,44 @@
|
|
|
1
|
-
"""Side-channel
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
For existing code using DPAAnalyzer with attack_type parameter, continue using
|
|
28
|
-
oscura.side_channel.dpa until migration to the new API.
|
|
29
|
-
|
|
30
|
-
Example (new API - recommended):
|
|
31
|
-
>>> from oscura.analyzers.side_channel import DPAAnalyzer, CPAAnalyzer
|
|
32
|
-
>>> # DPA attack
|
|
33
|
-
>>> dpa = DPAAnalyzer(target_bit=0)
|
|
34
|
-
>>> result = dpa.analyze(traces, plaintexts)
|
|
35
|
-
>>> # CPA attack
|
|
36
|
-
>>> cpa = CPAAnalyzer(leakage_model="hamming_weight")
|
|
37
|
-
>>> result = cpa.analyze(traces, plaintexts)
|
|
38
|
-
|
|
39
|
-
Example (old API - deprecated):
|
|
40
|
-
>>> from oscura.side_channel.dpa import DPAAnalyzer, PowerTrace
|
|
41
|
-
>>> analyzer = DPAAnalyzer(attack_type="cpa", leakage_model="hamming_weight")
|
|
42
|
-
>>> traces = [PowerTrace(timestamp=t, power=p, plaintext=pt) for ...]
|
|
43
|
-
>>> result = analyzer.perform_attack(traces, target_byte=0)
|
|
1
|
+
"""Side-channel trace loading.
|
|
2
|
+
|
|
3
|
+
This module ONLY provides ChipWhisperer trace loading for integration
|
|
4
|
+
into Oscura workflows.
|
|
5
|
+
|
|
6
|
+
For actual side-channel attacks, use ChipWhisperer directly:
|
|
7
|
+
https://chipwhisperer.com/
|
|
8
|
+
|
|
9
|
+
What's here:
|
|
10
|
+
- ChipWhisperer .npy/.trs trace loading via oscura.loaders.chipwhisperer
|
|
11
|
+
|
|
12
|
+
What's NOT here (use ChipWhisperer instead):
|
|
13
|
+
- DPA/CPA attacks
|
|
14
|
+
- Key recovery
|
|
15
|
+
- Leakage assessment
|
|
16
|
+
- Template attacks
|
|
17
|
+
- Hardware interfacing
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> from oscura.loaders.chipwhisperer import load_chipwhisperer
|
|
21
|
+
>>> traceset = load_chipwhisperer("capture_data.npy")
|
|
22
|
+
>>> print(f"Loaded {traceset.n_traces} traces")
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
ChipWhisperer Project: https://chipwhisperer.com/
|
|
26
|
+
ChipWhisperer Documentation: https://chipwhisperer.readthedocs.io/
|
|
44
27
|
"""
|
|
45
28
|
|
|
46
29
|
from __future__ import annotations
|
|
47
30
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"See migration guide in module docstring.",
|
|
55
|
-
DeprecationWarning,
|
|
56
|
-
stacklevel=2,
|
|
31
|
+
# Re-export ChipWhisperer loader for convenience
|
|
32
|
+
from oscura.loaders.chipwhisperer import (
|
|
33
|
+
ChipWhispererTraceSet,
|
|
34
|
+
load_chipwhisperer,
|
|
35
|
+
load_chipwhisperer_npy,
|
|
36
|
+
load_chipwhisperer_trs,
|
|
57
37
|
)
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ChipWhispererTraceSet",
|
|
41
|
+
"load_chipwhisperer",
|
|
42
|
+
"load_chipwhisperer_npy",
|
|
43
|
+
"load_chipwhisperer_trs",
|
|
44
|
+
]
|
|
@@ -940,7 +940,7 @@ class SignalBuilder:
|
|
|
940
940
|
# Build TraceMetadata
|
|
941
941
|
trace_metadata = TraceMetadata(
|
|
942
942
|
sample_rate=self._sample_rate,
|
|
943
|
-
|
|
943
|
+
channel=channel,
|
|
944
944
|
)
|
|
945
945
|
|
|
946
946
|
return WaveformTrace(data=data, metadata=trace_metadata)
|
|
@@ -977,12 +977,12 @@ class SignalBuilder:
|
|
|
977
977
|
|
|
978
978
|
# Build WaveformTrace for each channel
|
|
979
979
|
traces: dict[str, WaveformTrace] = {}
|
|
980
|
-
for
|
|
980
|
+
for channel, data in self._channels.items():
|
|
981
981
|
trace_metadata = TraceMetadata(
|
|
982
982
|
sample_rate=self._sample_rate,
|
|
983
|
-
|
|
983
|
+
channel=channel,
|
|
984
984
|
)
|
|
985
|
-
traces[
|
|
985
|
+
traces[channel] = WaveformTrace(data=data, metadata=trace_metadata)
|
|
986
986
|
|
|
987
987
|
return traces
|
|
988
988
|
|
|
@@ -1010,7 +1010,7 @@ class SignalBuilder:
|
|
|
1010
1010
|
path,
|
|
1011
1011
|
data=trace.data,
|
|
1012
1012
|
sample_rate=trace.metadata.sample_rate,
|
|
1013
|
-
|
|
1013
|
+
channel=trace.metadata.channel or "ch1",
|
|
1014
1014
|
)
|
|
1015
1015
|
|
|
1016
1016
|
return trace
|
|
@@ -59,7 +59,7 @@ def difference(
|
|
|
59
59
|
trace2: WaveformTrace,
|
|
60
60
|
*,
|
|
61
61
|
normalize: bool = False,
|
|
62
|
-
|
|
62
|
+
channel: str | None = None,
|
|
63
63
|
) -> WaveformTrace:
|
|
64
64
|
"""Compute difference between two traces.
|
|
65
65
|
|
|
@@ -70,7 +70,7 @@ def difference(
|
|
|
70
70
|
trace1: First trace.
|
|
71
71
|
trace2: Second trace.
|
|
72
72
|
normalize: Normalize difference to percentage of reference range.
|
|
73
|
-
|
|
73
|
+
channel: Name for the result trace.
|
|
74
74
|
|
|
75
75
|
Returns:
|
|
76
76
|
WaveformTrace containing the difference.
|
|
@@ -106,12 +106,10 @@ def difference(
|
|
|
106
106
|
|
|
107
107
|
new_metadata = TraceMetadata(
|
|
108
108
|
sample_rate=trace1.metadata.sample_rate,
|
|
109
|
-
vertical_scale=
|
|
110
|
-
vertical_offset=
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
source_file=trace1.metadata.source_file,
|
|
114
|
-
channel_name=channel_name or "difference",
|
|
109
|
+
vertical_scale=trace1.metadata.vertical_scale,
|
|
110
|
+
vertical_offset=trace1.metadata.vertical_offset,
|
|
111
|
+
channel=channel or "difference",
|
|
112
|
+
units=trace1.metadata.units,
|
|
115
113
|
)
|
|
116
114
|
|
|
117
115
|
return WaveformTrace(data=diff, metadata=new_metadata)
|
|
@@ -482,7 +480,7 @@ def compare_traces(
|
|
|
482
480
|
match = _determine_match(method, max_diff, tolerance, tolerance_pct, data1, data2)
|
|
483
481
|
|
|
484
482
|
diff_trace = (
|
|
485
|
-
difference(trace1, trace2,
|
|
483
|
+
difference(trace1, trace2, channel="comparison_diff") if include_difference else None
|
|
486
484
|
)
|
|
487
485
|
statistics = _compute_comparison_statistics(diff, violations, min_len, data1, data2)
|
|
488
486
|
|
|
@@ -496,7 +496,7 @@ def differentiate(
|
|
|
496
496
|
if order < 1:
|
|
497
497
|
raise AnalysisError(f"Derivative order must be positive, got {order}")
|
|
498
498
|
|
|
499
|
-
sample_period = trace.metadata.
|
|
499
|
+
sample_period = 1.0 / trace.metadata.sample_rate
|
|
500
500
|
result = trace.data.copy()
|
|
501
501
|
|
|
502
502
|
for _ in range(order):
|
|
@@ -530,7 +530,7 @@ def integrate(
|
|
|
530
530
|
Example:
|
|
531
531
|
>>> position = integrate(velocity_trace)
|
|
532
532
|
"""
|
|
533
|
-
sample_period = trace.metadata.
|
|
533
|
+
sample_period = 1.0 / trace.metadata.sample_rate
|
|
534
534
|
|
|
535
535
|
if method == "cumtrapz":
|
|
536
536
|
from scipy.integrate import cumulative_trapezoid
|
oscura/utils/math/arithmetic.py
CHANGED
|
@@ -73,7 +73,7 @@ def add(
|
|
|
73
73
|
trace1: WaveformTrace,
|
|
74
74
|
trace2: TraceOrScalar,
|
|
75
75
|
*,
|
|
76
|
-
|
|
76
|
+
channel: str | None = None,
|
|
77
77
|
) -> WaveformTrace:
|
|
78
78
|
"""Add two traces or add a scalar to a trace.
|
|
79
79
|
|
|
@@ -83,7 +83,7 @@ def add(
|
|
|
83
83
|
Args:
|
|
84
84
|
trace1: First trace (base trace).
|
|
85
85
|
trace2: Second trace or scalar value to add.
|
|
86
|
-
|
|
86
|
+
channel: Name for the result trace (optional).
|
|
87
87
|
|
|
88
88
|
Returns:
|
|
89
89
|
New WaveformTrace containing the sum.
|
|
@@ -121,10 +121,7 @@ def add(
|
|
|
121
121
|
sample_rate=metadata.sample_rate,
|
|
122
122
|
vertical_scale=metadata.vertical_scale,
|
|
123
123
|
vertical_offset=metadata.vertical_offset,
|
|
124
|
-
|
|
125
|
-
trigger_info=metadata.trigger_info,
|
|
126
|
-
source_file=metadata.source_file,
|
|
127
|
-
channel_name=channel_name or f"{metadata.channel_name or 'trace'}_sum",
|
|
124
|
+
channel=channel or f"{metadata.channel or 'trace'}_sum",
|
|
128
125
|
)
|
|
129
126
|
|
|
130
127
|
return WaveformTrace(data=result_data, metadata=new_metadata)
|
|
@@ -134,7 +131,7 @@ def subtract(
|
|
|
134
131
|
trace1: WaveformTrace,
|
|
135
132
|
trace2: TraceOrScalar,
|
|
136
133
|
*,
|
|
137
|
-
|
|
134
|
+
channel: str | None = None,
|
|
138
135
|
) -> WaveformTrace:
|
|
139
136
|
"""Subtract second trace from first trace or subtract a scalar.
|
|
140
137
|
|
|
@@ -144,7 +141,7 @@ def subtract(
|
|
|
144
141
|
Args:
|
|
145
142
|
trace1: Trace to subtract from.
|
|
146
143
|
trace2: Trace or scalar to subtract.
|
|
147
|
-
|
|
144
|
+
channel: Name for the result trace (optional).
|
|
148
145
|
|
|
149
146
|
Returns:
|
|
150
147
|
New WaveformTrace containing the difference.
|
|
@@ -178,10 +175,7 @@ def subtract(
|
|
|
178
175
|
sample_rate=metadata.sample_rate,
|
|
179
176
|
vertical_scale=metadata.vertical_scale,
|
|
180
177
|
vertical_offset=metadata.vertical_offset,
|
|
181
|
-
|
|
182
|
-
trigger_info=metadata.trigger_info,
|
|
183
|
-
source_file=metadata.source_file,
|
|
184
|
-
channel_name=channel_name or f"{metadata.channel_name or 'trace'}_diff",
|
|
178
|
+
channel=channel or f"{metadata.channel or 'trace'}_diff",
|
|
185
179
|
)
|
|
186
180
|
|
|
187
181
|
return WaveformTrace(data=result_data, metadata=new_metadata)
|
|
@@ -191,7 +185,7 @@ def multiply(
|
|
|
191
185
|
trace1: WaveformTrace,
|
|
192
186
|
trace2: TraceOrScalar,
|
|
193
187
|
*,
|
|
194
|
-
|
|
188
|
+
channel: str | None = None,
|
|
195
189
|
) -> WaveformTrace:
|
|
196
190
|
"""Multiply two traces or multiply trace by a scalar.
|
|
197
191
|
|
|
@@ -201,7 +195,7 @@ def multiply(
|
|
|
201
195
|
Args:
|
|
202
196
|
trace1: First trace.
|
|
203
197
|
trace2: Second trace or scalar multiplier.
|
|
204
|
-
|
|
198
|
+
channel: Name for the result trace (optional).
|
|
205
199
|
|
|
206
200
|
Returns:
|
|
207
201
|
New WaveformTrace containing the product.
|
|
@@ -235,10 +229,7 @@ def multiply(
|
|
|
235
229
|
sample_rate=metadata.sample_rate,
|
|
236
230
|
vertical_scale=metadata.vertical_scale,
|
|
237
231
|
vertical_offset=metadata.vertical_offset,
|
|
238
|
-
|
|
239
|
-
trigger_info=metadata.trigger_info,
|
|
240
|
-
source_file=metadata.source_file,
|
|
241
|
-
channel_name=channel_name or f"{metadata.channel_name or 'trace'}_mult",
|
|
232
|
+
channel=channel or f"{metadata.channel or 'trace'}_mult",
|
|
242
233
|
)
|
|
243
234
|
|
|
244
235
|
return WaveformTrace(data=result_data, metadata=new_metadata)
|
|
@@ -248,7 +239,7 @@ def divide(
|
|
|
248
239
|
trace1: WaveformTrace,
|
|
249
240
|
trace2: TraceOrScalar,
|
|
250
241
|
*,
|
|
251
|
-
|
|
242
|
+
channel: str | None = None,
|
|
252
243
|
fill_value: float = np.nan,
|
|
253
244
|
) -> WaveformTrace:
|
|
254
245
|
"""Divide first trace by second trace or by a scalar.
|
|
@@ -259,7 +250,7 @@ def divide(
|
|
|
259
250
|
Args:
|
|
260
251
|
trace1: Numerator trace.
|
|
261
252
|
trace2: Denominator trace or scalar.
|
|
262
|
-
|
|
253
|
+
channel: Name for the result trace (optional).
|
|
263
254
|
fill_value: Value to use for division by zero (default NaN).
|
|
264
255
|
|
|
265
256
|
Returns:
|
|
@@ -301,10 +292,7 @@ def divide(
|
|
|
301
292
|
sample_rate=metadata.sample_rate,
|
|
302
293
|
vertical_scale=metadata.vertical_scale,
|
|
303
294
|
vertical_offset=metadata.vertical_offset,
|
|
304
|
-
|
|
305
|
-
trigger_info=metadata.trigger_info,
|
|
306
|
-
source_file=metadata.source_file,
|
|
307
|
-
channel_name=channel_name or f"{metadata.channel_name or 'trace'}_div",
|
|
295
|
+
channel=channel or f"{metadata.channel or 'trace'}_div",
|
|
308
296
|
)
|
|
309
297
|
|
|
310
298
|
return WaveformTrace(data=result_data, metadata=new_metadata)
|
|
@@ -314,7 +302,7 @@ def scale(
|
|
|
314
302
|
trace: WaveformTrace,
|
|
315
303
|
factor: float,
|
|
316
304
|
*,
|
|
317
|
-
|
|
305
|
+
channel: str | None = None,
|
|
318
306
|
) -> WaveformTrace:
|
|
319
307
|
"""Scale trace by a constant factor.
|
|
320
308
|
|
|
@@ -324,7 +312,7 @@ def scale(
|
|
|
324
312
|
Args:
|
|
325
313
|
trace: Input trace.
|
|
326
314
|
factor: Scale factor to apply.
|
|
327
|
-
|
|
315
|
+
channel: Name for the result trace (optional).
|
|
328
316
|
|
|
329
317
|
Returns:
|
|
330
318
|
Scaled WaveformTrace.
|
|
@@ -336,7 +324,7 @@ def scale(
|
|
|
336
324
|
return multiply(
|
|
337
325
|
trace,
|
|
338
326
|
factor,
|
|
339
|
-
|
|
327
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_scaled",
|
|
340
328
|
)
|
|
341
329
|
|
|
342
330
|
|
|
@@ -344,7 +332,7 @@ def offset(
|
|
|
344
332
|
trace: WaveformTrace,
|
|
345
333
|
value: float,
|
|
346
334
|
*,
|
|
347
|
-
|
|
335
|
+
channel: str | None = None,
|
|
348
336
|
) -> WaveformTrace:
|
|
349
337
|
"""Add a constant offset to trace.
|
|
350
338
|
|
|
@@ -353,7 +341,7 @@ def offset(
|
|
|
353
341
|
Args:
|
|
354
342
|
trace: Input trace.
|
|
355
343
|
value: Offset value to add.
|
|
356
|
-
|
|
344
|
+
channel: Name for the result trace (optional).
|
|
357
345
|
|
|
358
346
|
Returns:
|
|
359
347
|
Offset WaveformTrace.
|
|
@@ -364,14 +352,14 @@ def offset(
|
|
|
364
352
|
return add(
|
|
365
353
|
trace,
|
|
366
354
|
value,
|
|
367
|
-
|
|
355
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_offset",
|
|
368
356
|
)
|
|
369
357
|
|
|
370
358
|
|
|
371
359
|
def invert(
|
|
372
360
|
trace: WaveformTrace,
|
|
373
361
|
*,
|
|
374
|
-
|
|
362
|
+
channel: str | None = None,
|
|
375
363
|
) -> WaveformTrace:
|
|
376
364
|
"""Invert trace polarity (multiply by -1).
|
|
377
365
|
|
|
@@ -379,7 +367,7 @@ def invert(
|
|
|
379
367
|
|
|
380
368
|
Args:
|
|
381
369
|
trace: Input trace.
|
|
382
|
-
|
|
370
|
+
channel: Name for the result trace (optional).
|
|
383
371
|
|
|
384
372
|
Returns:
|
|
385
373
|
Inverted WaveformTrace.
|
|
@@ -390,14 +378,14 @@ def invert(
|
|
|
390
378
|
return scale(
|
|
391
379
|
trace,
|
|
392
380
|
-1.0,
|
|
393
|
-
|
|
381
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_inverted",
|
|
394
382
|
)
|
|
395
383
|
|
|
396
384
|
|
|
397
385
|
def absolute(
|
|
398
386
|
trace: WaveformTrace,
|
|
399
387
|
*,
|
|
400
|
-
|
|
388
|
+
channel: str | None = None,
|
|
401
389
|
) -> WaveformTrace:
|
|
402
390
|
"""Compute absolute value of trace.
|
|
403
391
|
|
|
@@ -405,7 +393,7 @@ def absolute(
|
|
|
405
393
|
|
|
406
394
|
Args:
|
|
407
395
|
trace: Input trace.
|
|
408
|
-
|
|
396
|
+
channel: Name for the result trace (optional).
|
|
409
397
|
|
|
410
398
|
Returns:
|
|
411
399
|
WaveformTrace with absolute values.
|
|
@@ -419,10 +407,7 @@ def absolute(
|
|
|
419
407
|
sample_rate=trace.metadata.sample_rate,
|
|
420
408
|
vertical_scale=trace.metadata.vertical_scale,
|
|
421
409
|
vertical_offset=trace.metadata.vertical_offset,
|
|
422
|
-
|
|
423
|
-
trigger_info=trace.metadata.trigger_info,
|
|
424
|
-
source_file=trace.metadata.source_file,
|
|
425
|
-
channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_abs",
|
|
410
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_abs",
|
|
426
411
|
)
|
|
427
412
|
|
|
428
413
|
return WaveformTrace(data=result_data, metadata=new_metadata)
|
|
@@ -433,7 +418,7 @@ def differentiate(
|
|
|
433
418
|
*,
|
|
434
419
|
order: int = 1,
|
|
435
420
|
method: str = "central",
|
|
436
|
-
|
|
421
|
+
channel: str | None = None,
|
|
437
422
|
) -> WaveformTrace:
|
|
438
423
|
"""Compute numerical derivative of trace.
|
|
439
424
|
|
|
@@ -447,7 +432,7 @@ def differentiate(
|
|
|
447
432
|
- "central": Central difference (default, most accurate)
|
|
448
433
|
- "forward": Forward difference
|
|
449
434
|
- "backward": Backward difference
|
|
450
|
-
|
|
435
|
+
channel: Name for the result trace (optional).
|
|
451
436
|
|
|
452
437
|
Returns:
|
|
453
438
|
Differentiated WaveformTrace in V/s.
|
|
@@ -467,7 +452,7 @@ def differentiate(
|
|
|
467
452
|
raise ValueError(f"Order must be positive, got {order}")
|
|
468
453
|
|
|
469
454
|
data = trace.data.astype(np.float64)
|
|
470
|
-
dt = trace.metadata.
|
|
455
|
+
dt = 1.0 / trace.metadata.sample_rate
|
|
471
456
|
|
|
472
457
|
if len(data) < order + 1:
|
|
473
458
|
raise InsufficientDataError(
|
|
@@ -500,10 +485,7 @@ def differentiate(
|
|
|
500
485
|
sample_rate=trace.metadata.sample_rate,
|
|
501
486
|
vertical_scale=None, # Units changed
|
|
502
487
|
vertical_offset=None,
|
|
503
|
-
|
|
504
|
-
trigger_info=trace.metadata.trigger_info,
|
|
505
|
-
source_file=trace.metadata.source_file,
|
|
506
|
-
channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_d{order}",
|
|
488
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_d{order}",
|
|
507
489
|
)
|
|
508
490
|
|
|
509
491
|
return WaveformTrace(data=result, metadata=new_metadata)
|
|
@@ -514,7 +496,7 @@ def integrate(
|
|
|
514
496
|
*,
|
|
515
497
|
method: str = "trapezoid",
|
|
516
498
|
initial: float = 0.0,
|
|
517
|
-
|
|
499
|
+
channel: str | None = None,
|
|
518
500
|
) -> WaveformTrace:
|
|
519
501
|
"""Compute numerical integral of trace.
|
|
520
502
|
|
|
@@ -528,7 +510,7 @@ def integrate(
|
|
|
528
510
|
- "simpson": Simpson's rule (requires odd number of points)
|
|
529
511
|
- "cumsum": Simple cumulative sum
|
|
530
512
|
initial: Initial value for cumulative integral (default 0).
|
|
531
|
-
|
|
513
|
+
channel: Name for the result trace (optional).
|
|
532
514
|
|
|
533
515
|
Returns:
|
|
534
516
|
Integrated WaveformTrace in V*s.
|
|
@@ -545,7 +527,7 @@ def integrate(
|
|
|
545
527
|
ARITH-006
|
|
546
528
|
"""
|
|
547
529
|
data = trace.data.astype(np.float64)
|
|
548
|
-
dt = trace.metadata.
|
|
530
|
+
dt = 1.0 / trace.metadata.sample_rate
|
|
549
531
|
|
|
550
532
|
if len(data) < 2:
|
|
551
533
|
raise InsufficientDataError(
|
|
@@ -572,10 +554,7 @@ def integrate(
|
|
|
572
554
|
sample_rate=trace.metadata.sample_rate,
|
|
573
555
|
vertical_scale=None, # Units changed
|
|
574
556
|
vertical_offset=None,
|
|
575
|
-
|
|
576
|
-
trigger_info=trace.metadata.trigger_info,
|
|
577
|
-
source_file=trace.metadata.source_file,
|
|
578
|
-
channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_integral",
|
|
557
|
+
channel=channel or f"{trace.metadata.channel or 'trace'}_integral",
|
|
579
558
|
)
|
|
580
559
|
|
|
581
560
|
return WaveformTrace(data=result, metadata=new_metadata)
|
|
@@ -818,14 +797,14 @@ def _ensure_array_result(result: Any, expected_len: int) -> NDArray[np.float64]:
|
|
|
818
797
|
|
|
819
798
|
|
|
820
799
|
def _build_expression_metadata(
|
|
821
|
-
ref_trace: WaveformTrace, expression: str,
|
|
800
|
+
ref_trace: WaveformTrace, expression: str, channel: str | None
|
|
822
801
|
) -> TraceMetadata:
|
|
823
802
|
"""Build metadata for expression result trace.
|
|
824
803
|
|
|
825
804
|
Args:
|
|
826
805
|
ref_trace: Reference trace for metadata.
|
|
827
806
|
expression: Expression string (for default naming).
|
|
828
|
-
|
|
807
|
+
channel: Optional channel name override.
|
|
829
808
|
|
|
830
809
|
Returns:
|
|
831
810
|
Metadata for result trace.
|
|
@@ -834,10 +813,7 @@ def _build_expression_metadata(
|
|
|
834
813
|
sample_rate=ref_trace.metadata.sample_rate,
|
|
835
814
|
vertical_scale=None,
|
|
836
815
|
vertical_offset=None,
|
|
837
|
-
|
|
838
|
-
trigger_info=ref_trace.metadata.trigger_info,
|
|
839
|
-
source_file=ref_trace.metadata.source_file,
|
|
840
|
-
channel_name=channel_name or f"expr({expression[:20]})",
|
|
816
|
+
channel=channel or f"expr({expression[:20]})",
|
|
841
817
|
)
|
|
842
818
|
|
|
843
819
|
|
|
@@ -845,7 +821,7 @@ def math_expression(
|
|
|
845
821
|
expression: str,
|
|
846
822
|
traces: dict[str, WaveformTrace],
|
|
847
823
|
*,
|
|
848
|
-
|
|
824
|
+
channel: str | None = None,
|
|
849
825
|
) -> WaveformTrace:
|
|
850
826
|
"""Evaluate a mathematical expression on traces.
|
|
851
827
|
|
|
@@ -855,7 +831,7 @@ def math_expression(
|
|
|
855
831
|
Args:
|
|
856
832
|
expression: Math expression (e.g., "CH1 + CH2", "abs(CH1 - CH2)").
|
|
857
833
|
traces: Dictionary mapping variable names to traces.
|
|
858
|
-
|
|
834
|
+
channel: Name for the result trace (optional).
|
|
859
835
|
|
|
860
836
|
Returns:
|
|
861
837
|
Result WaveformTrace.
|
|
@@ -884,5 +860,5 @@ def math_expression(
|
|
|
884
860
|
result = _evaluate_expression(expression, safe_namespace)
|
|
885
861
|
result = _ensure_array_result(result, len(ref_trace.data))
|
|
886
862
|
|
|
887
|
-
metadata = _build_expression_metadata(ref_trace, expression,
|
|
863
|
+
metadata = _build_expression_metadata(ref_trace, expression, channel)
|
|
888
864
|
return WaveformTrace(data=result.astype(np.float64), metadata=metadata)
|