oscura 0.1.2__py3-none-any.whl → 0.3.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 +1 -1
- oscura/analyzers/packet/payload_extraction.py +2 -4
- oscura/analyzers/packet/stream.py +5 -5
- oscura/analyzers/patterns/__init__.py +4 -3
- oscura/analyzers/patterns/clustering.py +3 -1
- oscura/analyzers/power/ac_power.py +0 -2
- oscura/analyzers/power/basic.py +0 -2
- oscura/analyzers/power/ripple.py +0 -2
- oscura/analyzers/signal_integrity/embedding.py +0 -2
- oscura/analyzers/signal_integrity/sparams.py +28 -206
- oscura/analyzers/spectral/fft.py +0 -2
- oscura/analyzers/statistical/__init__.py +3 -3
- oscura/analyzers/statistical/checksum.py +2 -0
- oscura/analyzers/statistical/classification.py +2 -0
- oscura/analyzers/statistical/entropy.py +11 -9
- oscura/analyzers/statistical/ngrams.py +4 -2
- oscura/api/fluent.py +2 -2
- oscura/automotive/__init__.py +1 -3
- oscura/automotive/can/__init__.py +0 -2
- oscura/automotive/dbc/__init__.py +0 -2
- oscura/automotive/dtc/__init__.py +0 -2
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +37 -2769
- oscura/automotive/j1939/__init__.py +0 -2
- oscura/automotive/loaders/__init__.py +0 -2
- oscura/automotive/loaders/asc.py +0 -2
- oscura/automotive/loaders/blf.py +0 -2
- oscura/automotive/loaders/csv_can.py +0 -2
- oscura/automotive/obd/__init__.py +0 -2
- oscura/automotive/uds/__init__.py +0 -2
- oscura/automotive/uds/models.py +0 -2
- oscura/cli/main.py +0 -2
- oscura/cli/shell.py +0 -2
- oscura/config/loader.py +0 -2
- oscura/core/backend_selector.py +1 -1
- oscura/core/correlation.py +0 -2
- oscura/core/exceptions.py +56 -2
- oscura/core/lazy.py +5 -3
- oscura/core/memory_limits.py +0 -2
- oscura/core/numba_backend.py +5 -7
- oscura/core/uncertainty.py +3 -3
- oscura/dsl/interpreter.py +2 -0
- oscura/dsl/parser.py +8 -6
- oscura/exploratory/error_recovery.py +3 -3
- oscura/exploratory/parse.py +2 -0
- oscura/exploratory/recovery.py +2 -0
- oscura/exploratory/sync.py +2 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/type_mapping.py +2 -0
- oscura/exporters/hdf5.py +1 -3
- oscura/extensibility/templates.py +0 -8
- oscura/inference/active_learning/lstar.py +2 -4
- oscura/inference/active_learning/observation_table.py +0 -2
- oscura/inference/active_learning/oracle.py +3 -1
- oscura/inference/active_learning/teachers/simulator.py +1 -3
- oscura/inference/alignment.py +2 -0
- oscura/inference/message_format.py +2 -0
- oscura/inference/protocol_dsl.py +7 -5
- oscura/inference/sequences.py +12 -14
- oscura/inference/state_machine.py +2 -0
- oscura/integrations/llm.py +3 -1
- oscura/jupyter/display.py +0 -2
- oscura/loaders/__init__.py +67 -51
- oscura/loaders/pcap.py +1 -1
- oscura/loaders/touchstone.py +221 -0
- oscura/math/arithmetic.py +0 -2
- oscura/optimization/parallel.py +9 -6
- oscura/pipeline/composition.py +0 -2
- oscura/plugins/cli.py +0 -2
- oscura/reporting/comparison.py +0 -2
- oscura/reporting/config.py +1 -1
- oscura/reporting/formatting/emphasis.py +2 -0
- oscura/reporting/formatting/numbers.py +0 -2
- oscura/reporting/output.py +1 -3
- oscura/reporting/sections.py +0 -2
- oscura/search/anomaly.py +2 -0
- oscura/session/session.py +80 -13
- oscura/testing/synthetic.py +2 -0
- oscura/ui/formatters.py +4 -2
- oscura/utils/buffer.py +2 -2
- oscura/utils/lazy.py +5 -5
- oscura/utils/memory_advanced.py +2 -2
- oscura/utils/memory_extensions.py +2 -2
- oscura/visualization/colors.py +0 -2
- oscura/visualization/power.py +2 -0
- oscura/workflows/multi_trace.py +2 -0
- {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/METADATA +37 -16
- {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/RECORD +91 -89
- {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/WHEEL +0 -0
- {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/licenses/LICENSE +0 -0
oscura/inference/sequences.py
CHANGED
|
@@ -8,8 +8,6 @@ streams, identifying request-response pairs, and analyzing communication
|
|
|
8
8
|
flows.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
11
|
from collections import defaultdict
|
|
14
12
|
from collections.abc import Callable, Sequence
|
|
15
13
|
from dataclasses import dataclass, field
|
|
@@ -83,7 +81,7 @@ class CommunicationFlow:
|
|
|
83
81
|
|
|
84
82
|
flow_id: int
|
|
85
83
|
messages: list[Any]
|
|
86
|
-
pairs: list[RequestResponsePair]
|
|
84
|
+
pairs: list["RequestResponsePair"]
|
|
87
85
|
direction: str
|
|
88
86
|
participants: list[str]
|
|
89
87
|
duration: float
|
|
@@ -127,7 +125,7 @@ class SequencePatternDetector:
|
|
|
127
125
|
messages: Sequence[Any],
|
|
128
126
|
key: Callable[[Any], Any] | None = None,
|
|
129
127
|
timestamp_key: Callable[[Any], float] | None = None,
|
|
130
|
-
) -> list[SequencePattern]:
|
|
128
|
+
) -> list["SequencePattern"]:
|
|
131
129
|
"""Detect sequential patterns in message stream.
|
|
132
130
|
|
|
133
131
|
Implements RE-SEQ-002: Pattern detection workflow.
|
|
@@ -222,7 +220,7 @@ class SequencePatternDetector:
|
|
|
222
220
|
messages: Sequence[Any],
|
|
223
221
|
key: Callable[[Any], Any] | None = None,
|
|
224
222
|
timestamp_key: Callable[[Any], float] | None = None,
|
|
225
|
-
) -> list[SequencePattern]:
|
|
223
|
+
) -> list["SequencePattern"]:
|
|
226
224
|
"""Detect patterns that occur at regular intervals.
|
|
227
225
|
|
|
228
226
|
Implements RE-SEQ-002: Periodic pattern detection.
|
|
@@ -287,7 +285,7 @@ class SequencePatternDetector:
|
|
|
287
285
|
candidates: dict[tuple[Any, ...], list[int]],
|
|
288
286
|
identifiers: list[Any],
|
|
289
287
|
timestamps: list[float] | None,
|
|
290
|
-
) -> list[SequencePattern]:
|
|
288
|
+
) -> list["SequencePattern"]:
|
|
291
289
|
"""Score candidate patterns.
|
|
292
290
|
|
|
293
291
|
Args:
|
|
@@ -386,7 +384,7 @@ class RequestResponseCorrelator:
|
|
|
386
384
|
request_filter: Callable[[Any], bool] | None = None,
|
|
387
385
|
response_filter: Callable[[Any], bool] | None = None,
|
|
388
386
|
timestamp_key: Callable[[Any], float] | None = None,
|
|
389
|
-
) -> list[RequestResponsePair]:
|
|
387
|
+
) -> list["RequestResponsePair"]:
|
|
390
388
|
"""Correlate requests with responses.
|
|
391
389
|
|
|
392
390
|
Implements RE-SEQ-003: Request-response correlation workflow.
|
|
@@ -441,7 +439,7 @@ class RequestResponseCorrelator:
|
|
|
441
439
|
messages: Sequence[Any],
|
|
442
440
|
content_key: Callable[[Any], bytes],
|
|
443
441
|
timestamp_key: Callable[[Any], float] | None = None,
|
|
444
|
-
) -> list[RequestResponsePair]:
|
|
442
|
+
) -> list["RequestResponsePair"]:
|
|
445
443
|
"""Correlate by analyzing message content similarity.
|
|
446
444
|
|
|
447
445
|
Implements RE-SEQ-003: Content-based correlation.
|
|
@@ -500,10 +498,10 @@ class RequestResponseCorrelator:
|
|
|
500
498
|
|
|
501
499
|
def extract_flows(
|
|
502
500
|
self,
|
|
503
|
-
pairs: Sequence[RequestResponsePair],
|
|
501
|
+
pairs: Sequence["RequestResponsePair"],
|
|
504
502
|
messages: Sequence[Any],
|
|
505
503
|
flow_key: Callable[[Any], str] | None = None,
|
|
506
|
-
) -> list[CommunicationFlow]:
|
|
504
|
+
) -> list["CommunicationFlow"]:
|
|
507
505
|
"""Extract communication flows from pairs.
|
|
508
506
|
|
|
509
507
|
Implements RE-SEQ-003: Flow extraction.
|
|
@@ -569,7 +567,7 @@ class RequestResponseCorrelator:
|
|
|
569
567
|
self,
|
|
570
568
|
requests: list[tuple[int, Any, float, Any]],
|
|
571
569
|
responses: list[tuple[int, Any, float, Any]],
|
|
572
|
-
) -> list[RequestResponsePair]:
|
|
570
|
+
) -> list["RequestResponsePair"]:
|
|
573
571
|
"""Match request and response messages.
|
|
574
572
|
|
|
575
573
|
Args:
|
|
@@ -664,7 +662,7 @@ def detect_sequence_patterns(
|
|
|
664
662
|
min_length: int = 2,
|
|
665
663
|
max_length: int = 10,
|
|
666
664
|
min_frequency: int = 2,
|
|
667
|
-
) -> list[SequencePattern]:
|
|
665
|
+
) -> list["SequencePattern"]:
|
|
668
666
|
"""Detect sequential patterns in messages.
|
|
669
667
|
|
|
670
668
|
Implements RE-SEQ-002: Sequence Pattern Detection.
|
|
@@ -699,7 +697,7 @@ def correlate_requests(
|
|
|
699
697
|
response_filter: Callable[[Any], bool],
|
|
700
698
|
timestamp_key: Callable[[Any], float] | None = None,
|
|
701
699
|
max_latency: float = 10.0,
|
|
702
|
-
) -> list[RequestResponsePair]:
|
|
700
|
+
) -> list["RequestResponsePair"]:
|
|
703
701
|
"""Correlate request and response messages.
|
|
704
702
|
|
|
705
703
|
Implements RE-SEQ-003: Request-Response Correlation.
|
|
@@ -760,7 +758,7 @@ def find_message_dependencies(
|
|
|
760
758
|
|
|
761
759
|
|
|
762
760
|
def calculate_latency_stats(
|
|
763
|
-
pairs: Sequence[RequestResponsePair],
|
|
761
|
+
pairs: Sequence["RequestResponsePair"],
|
|
764
762
|
) -> dict[str, float]:
|
|
765
763
|
"""Calculate latency statistics for request-response pairs.
|
|
766
764
|
|
oscura/integrations/llm.py
CHANGED
|
@@ -23,6 +23,8 @@ Examples:
|
|
|
23
23
|
... )
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
26
28
|
import hashlib
|
|
27
29
|
import json
|
|
28
30
|
import os
|
|
@@ -1545,7 +1547,7 @@ def get_client_auto(**config_kwargs: Any) -> LLMClient:
|
|
|
1545
1547
|
|
|
1546
1548
|
def get_client_with_failover(
|
|
1547
1549
|
providers: list[str] | None = None, **config_kwargs: Any
|
|
1548
|
-
) ->
|
|
1550
|
+
) -> FailoverLLMClient:
|
|
1549
1551
|
"""Get LLM client with automatic failover between providers.
|
|
1550
1552
|
|
|
1551
1553
|
Failover logic (try OpenAI, fallback to Anthropic).
|
oscura/jupyter/display.py
CHANGED
oscura/loaders/__init__.py
CHANGED
|
@@ -20,10 +20,73 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import warnings
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import TYPE_CHECKING, Any
|
|
23
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
24
24
|
|
|
25
25
|
from oscura.core.exceptions import LoaderError, UnsupportedFormatError
|
|
26
|
-
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
26
|
+
from oscura.core.types import DigitalTrace, IQTrace, WaveformTrace
|
|
27
|
+
|
|
28
|
+
# Loader registry for cleaner dispatch
|
|
29
|
+
_LOADER_REGISTRY: dict[str, tuple[str, str]] = {
|
|
30
|
+
"tektronix": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
|
|
31
|
+
"tek": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
|
|
32
|
+
"rigol": ("oscura.loaders.rigol", "load_rigol_wfm"),
|
|
33
|
+
"numpy": ("oscura.loaders.numpy_loader", "load_npz"),
|
|
34
|
+
"csv": ("oscura.loaders.csv_loader", "load_csv"),
|
|
35
|
+
"hdf5": ("oscura.loaders.hdf5_loader", "load_hdf5"),
|
|
36
|
+
"sigrok": ("oscura.loaders.sigrok", "load_sigrok"),
|
|
37
|
+
"vcd": ("oscura.loaders.vcd", "load_vcd"),
|
|
38
|
+
"pcap": ("oscura.loaders.pcap", "load_pcap"),
|
|
39
|
+
"wav": ("oscura.loaders.wav", "load_wav"),
|
|
40
|
+
"tdms": ("oscura.loaders.tdms", "load_tdms"),
|
|
41
|
+
"touchstone": ("oscura.loaders.touchstone", "load_touchstone"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _dispatch_loader(
|
|
46
|
+
loader_name: str, path: Path, **kwargs: Any
|
|
47
|
+
) -> WaveformTrace | DigitalTrace | IQTrace:
|
|
48
|
+
"""Dispatch to registered loader.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
loader_name: Name of loader to use.
|
|
52
|
+
path: Path to file.
|
|
53
|
+
**kwargs: Additional arguments for loader.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Loaded data.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
UnsupportedFormatError: If loader not registered.
|
|
60
|
+
"""
|
|
61
|
+
if loader_name not in _LOADER_REGISTRY:
|
|
62
|
+
raise UnsupportedFormatError(
|
|
63
|
+
loader_name,
|
|
64
|
+
list(_LOADER_REGISTRY.keys()),
|
|
65
|
+
file_path=str(path),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
module_path, func_name = _LOADER_REGISTRY[loader_name]
|
|
69
|
+
|
|
70
|
+
# Dynamically import the module
|
|
71
|
+
import importlib
|
|
72
|
+
import inspect
|
|
73
|
+
|
|
74
|
+
module = importlib.import_module(module_path)
|
|
75
|
+
loader_func = getattr(module, func_name)
|
|
76
|
+
|
|
77
|
+
# Filter kwargs to only include parameters the function accepts
|
|
78
|
+
sig = inspect.signature(loader_func)
|
|
79
|
+
valid_kwargs = {}
|
|
80
|
+
for key, value in kwargs.items():
|
|
81
|
+
if key in sig.parameters or any(
|
|
82
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
|
|
83
|
+
):
|
|
84
|
+
valid_kwargs[key] = value
|
|
85
|
+
|
|
86
|
+
# Call loader with appropriate arguments
|
|
87
|
+
result = loader_func(path, **valid_kwargs)
|
|
88
|
+
return cast("WaveformTrace | DigitalTrace | IQTrace", result)
|
|
89
|
+
|
|
27
90
|
|
|
28
91
|
# Import alias modules for DSL compatibility
|
|
29
92
|
from oscura.loaders import (
|
|
@@ -180,56 +243,9 @@ def load(
|
|
|
180
243
|
# Dispatch to appropriate loader
|
|
181
244
|
if loader_name == "auto_wfm":
|
|
182
245
|
return _load_wfm_auto(path, channel=channel, **kwargs)
|
|
183
|
-
elif loader_name in ("tektronix", "tek"):
|
|
184
|
-
from oscura.loaders.tektronix import load_tektronix_wfm
|
|
185
|
-
|
|
186
|
-
return load_tektronix_wfm(path, **kwargs)
|
|
187
|
-
elif loader_name == "rigol":
|
|
188
|
-
from oscura.loaders.rigol import load_rigol_wfm
|
|
189
|
-
|
|
190
|
-
return load_rigol_wfm(path, **kwargs)
|
|
191
|
-
elif loader_name == "numpy":
|
|
192
|
-
from oscura.loaders.numpy_loader import load_npz
|
|
193
|
-
|
|
194
|
-
return load_npz(path, channel=channel, **kwargs)
|
|
195
|
-
elif loader_name == "csv":
|
|
196
|
-
from oscura.loaders.csv_loader import load_csv
|
|
197
|
-
|
|
198
|
-
return load_csv(path, **kwargs) # type: ignore[return-value]
|
|
199
|
-
elif loader_name == "hdf5":
|
|
200
|
-
from oscura.loaders.hdf5_loader import load_hdf5
|
|
201
|
-
|
|
202
|
-
return load_hdf5(path, channel=channel, **kwargs) # type: ignore[return-value]
|
|
203
|
-
elif loader_name == "sigrok":
|
|
204
|
-
from oscura.loaders.sigrok import load_sigrok
|
|
205
|
-
|
|
206
|
-
return load_sigrok(path, channel=channel, **kwargs)
|
|
207
|
-
elif loader_name == "vcd":
|
|
208
|
-
from oscura.loaders.vcd import load_vcd
|
|
209
|
-
|
|
210
|
-
return load_vcd(path, **kwargs)
|
|
211
|
-
elif loader_name == "pcap":
|
|
212
|
-
from oscura.loaders.pcap import load_pcap
|
|
213
|
-
|
|
214
|
-
return load_pcap(path, **kwargs) # type: ignore[return-value]
|
|
215
|
-
elif loader_name == "wav":
|
|
216
|
-
from oscura.loaders.wav import load_wav
|
|
217
|
-
|
|
218
|
-
return load_wav(path, channel=channel, **kwargs)
|
|
219
|
-
elif loader_name == "tdms":
|
|
220
|
-
from oscura.loaders.tdms import load_tdms
|
|
221
|
-
|
|
222
|
-
return load_tdms(path, channel=channel, **kwargs)
|
|
223
|
-
elif loader_name == "touchstone":
|
|
224
|
-
from oscura.analyzers.signal_integrity.sparams import load_touchstone
|
|
225
|
-
|
|
226
|
-
return load_touchstone(path) # type: ignore[return-value]
|
|
227
246
|
else:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
list(SUPPORTED_FORMATS.keys()),
|
|
231
|
-
file_path=str(path),
|
|
232
|
-
)
|
|
247
|
+
# Use registry-based dispatch for all other loaders
|
|
248
|
+
return _dispatch_loader(loader_name, path, channel=channel, **kwargs)
|
|
233
249
|
|
|
234
250
|
|
|
235
251
|
def _load_wfm_auto(
|
oscura/loaders/pcap.py
CHANGED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Touchstone file loader for S-parameter data.
|
|
2
|
+
|
|
3
|
+
Supports .s1p through .s8p formats (Touchstone 1.0 and 2.0).
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> from oscura.loaders import load_touchstone
|
|
7
|
+
>>> s_params = load_touchstone("cable.s2p")
|
|
8
|
+
>>> print(f"Loaded {s_params.n_ports}-port, {len(s_params.frequencies)} points")
|
|
9
|
+
|
|
10
|
+
References:
|
|
11
|
+
Touchstone 2.0 File Format Specification
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import contextlib
|
|
17
|
+
import re
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from oscura.analyzers.signal_integrity.sparams import SParameterData
|
|
23
|
+
from oscura.core.exceptions import FormatError, LoaderError
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_touchstone(path: str | Path) -> SParameterData:
|
|
27
|
+
"""Load S-parameter data from Touchstone file.
|
|
28
|
+
|
|
29
|
+
Supports .s1p through .s8p formats and both Touchstone 1.0
|
|
30
|
+
and 2.0 file formats.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: Path to Touchstone file.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
SParameterData with loaded S-parameters.
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
LoaderError: If file cannot be read.
|
|
40
|
+
FormatError: If file format is invalid.
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
>>> s_params = load_touchstone("cable.s2p")
|
|
44
|
+
>>> print(f"Loaded {s_params.n_ports}-port, {len(s_params.frequencies)} points")
|
|
45
|
+
|
|
46
|
+
References:
|
|
47
|
+
Touchstone 2.0 File Format Specification
|
|
48
|
+
"""
|
|
49
|
+
path = Path(path)
|
|
50
|
+
|
|
51
|
+
if not path.exists():
|
|
52
|
+
raise LoaderError(f"File not found: {path}")
|
|
53
|
+
|
|
54
|
+
# Determine number of ports from extension
|
|
55
|
+
suffix = path.suffix.lower()
|
|
56
|
+
match = re.match(r"\.s(\d+)p", suffix)
|
|
57
|
+
if not match:
|
|
58
|
+
raise FormatError(f"Unsupported file extension: {suffix}")
|
|
59
|
+
|
|
60
|
+
n_ports = int(match.group(1))
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
with open(path) as f:
|
|
64
|
+
lines = f.readlines()
|
|
65
|
+
except Exception as e:
|
|
66
|
+
raise LoaderError(f"Failed to read file: {e}") # noqa: B904
|
|
67
|
+
|
|
68
|
+
return _parse_touchstone(lines, n_ports, str(path))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _parse_touchstone(
|
|
72
|
+
lines: list[str],
|
|
73
|
+
n_ports: int,
|
|
74
|
+
source_file: str,
|
|
75
|
+
) -> SParameterData:
|
|
76
|
+
"""Parse Touchstone file content.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
lines: File lines.
|
|
80
|
+
n_ports: Number of ports.
|
|
81
|
+
source_file: Source file path.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Parsed SParameterData.
|
|
85
|
+
|
|
86
|
+
Raises:
|
|
87
|
+
FormatError: If file format is invalid.
|
|
88
|
+
"""
|
|
89
|
+
comments = []
|
|
90
|
+
option_line = None
|
|
91
|
+
data_lines = []
|
|
92
|
+
|
|
93
|
+
for line in lines:
|
|
94
|
+
line = line.strip()
|
|
95
|
+
|
|
96
|
+
if not line:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if line.startswith("!"):
|
|
100
|
+
comments.append(line[1:].strip())
|
|
101
|
+
elif line.startswith("#"):
|
|
102
|
+
option_line = line
|
|
103
|
+
else:
|
|
104
|
+
data_lines.append(line)
|
|
105
|
+
|
|
106
|
+
# Parse option line
|
|
107
|
+
freq_unit = 1e9 # Default GHz
|
|
108
|
+
format_type = "ma" # Default MA (magnitude/angle)
|
|
109
|
+
z0 = 50.0
|
|
110
|
+
|
|
111
|
+
if option_line:
|
|
112
|
+
option_line = option_line.lower()
|
|
113
|
+
parts = option_line.split()
|
|
114
|
+
|
|
115
|
+
for i, part in enumerate(parts):
|
|
116
|
+
if part in ("hz", "khz", "mhz", "ghz"):
|
|
117
|
+
freq_unit = {
|
|
118
|
+
"hz": 1.0,
|
|
119
|
+
"khz": 1e3,
|
|
120
|
+
"mhz": 1e6,
|
|
121
|
+
"ghz": 1e9,
|
|
122
|
+
}[part]
|
|
123
|
+
elif part in ("db", "ma", "ri"):
|
|
124
|
+
format_type = part
|
|
125
|
+
elif part == "r":
|
|
126
|
+
# Reference impedance follows
|
|
127
|
+
if i + 1 < len(parts):
|
|
128
|
+
with contextlib.suppress(ValueError):
|
|
129
|
+
z0 = float(parts[i + 1])
|
|
130
|
+
|
|
131
|
+
# Parse data
|
|
132
|
+
frequencies = []
|
|
133
|
+
s_data = []
|
|
134
|
+
|
|
135
|
+
# Number of S-parameters per frequency
|
|
136
|
+
n_s_params = n_ports * n_ports
|
|
137
|
+
|
|
138
|
+
i = 0
|
|
139
|
+
while i < len(data_lines):
|
|
140
|
+
# First line has frequency and first S-parameters
|
|
141
|
+
parts = data_lines[i].split()
|
|
142
|
+
|
|
143
|
+
if len(parts) < 1:
|
|
144
|
+
i += 1
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
freq = float(parts[0]) * freq_unit
|
|
148
|
+
frequencies.append(freq)
|
|
149
|
+
|
|
150
|
+
# Collect all S-parameter values for this frequency
|
|
151
|
+
s_values = []
|
|
152
|
+
|
|
153
|
+
# Add values from first line
|
|
154
|
+
for j in range(1, len(parts), 2):
|
|
155
|
+
if j + 1 < len(parts):
|
|
156
|
+
val1 = float(parts[j])
|
|
157
|
+
val2 = float(parts[j + 1])
|
|
158
|
+
s_values.append((val1, val2))
|
|
159
|
+
|
|
160
|
+
i += 1
|
|
161
|
+
|
|
162
|
+
# Continue collecting from subsequent lines if needed
|
|
163
|
+
while len(s_values) < n_s_params and i < len(data_lines):
|
|
164
|
+
parts = data_lines[i].split()
|
|
165
|
+
|
|
166
|
+
# Check if this is a new frequency (has odd number of values)
|
|
167
|
+
try:
|
|
168
|
+
float(parts[0])
|
|
169
|
+
if len(parts) % 2 == 1:
|
|
170
|
+
break # New frequency line
|
|
171
|
+
except (ValueError, IndexError):
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
for j in range(0, len(parts), 2):
|
|
175
|
+
if j + 1 < len(parts):
|
|
176
|
+
val1 = float(parts[j])
|
|
177
|
+
val2 = float(parts[j + 1])
|
|
178
|
+
s_values.append((val1, val2))
|
|
179
|
+
|
|
180
|
+
i += 1
|
|
181
|
+
|
|
182
|
+
# Convert to complex based on format
|
|
183
|
+
s_complex = []
|
|
184
|
+
for val1, val2 in s_values:
|
|
185
|
+
if format_type == "ri":
|
|
186
|
+
# Real/Imaginary
|
|
187
|
+
s_complex.append(complex(val1, val2))
|
|
188
|
+
elif format_type == "ma":
|
|
189
|
+
# Magnitude/Angle (degrees)
|
|
190
|
+
mag = val1
|
|
191
|
+
angle_rad = np.radians(val2)
|
|
192
|
+
s_complex.append(mag * np.exp(1j * angle_rad))
|
|
193
|
+
elif format_type == "db":
|
|
194
|
+
# dB/Angle (degrees)
|
|
195
|
+
mag = 10 ** (val1 / 20)
|
|
196
|
+
angle_rad = np.radians(val2)
|
|
197
|
+
s_complex.append(mag * np.exp(1j * angle_rad))
|
|
198
|
+
|
|
199
|
+
# Reshape into matrix
|
|
200
|
+
if len(s_complex) == n_s_params:
|
|
201
|
+
s_matrix = np.array(s_complex).reshape(n_ports, n_ports)
|
|
202
|
+
s_data.append(s_matrix)
|
|
203
|
+
|
|
204
|
+
if len(frequencies) == 0:
|
|
205
|
+
raise FormatError("No valid frequency points found")
|
|
206
|
+
|
|
207
|
+
frequencies_arr = np.array(frequencies, dtype=np.float64)
|
|
208
|
+
s_matrix_arr = np.array(s_data, dtype=np.complex128)
|
|
209
|
+
|
|
210
|
+
return SParameterData(
|
|
211
|
+
frequencies=frequencies_arr,
|
|
212
|
+
s_matrix=s_matrix_arr,
|
|
213
|
+
n_ports=n_ports,
|
|
214
|
+
z0=z0,
|
|
215
|
+
format=format_type,
|
|
216
|
+
source_file=source_file,
|
|
217
|
+
comments=comments,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
__all__ = ["load_touchstone"]
|
oscura/math/arithmetic.py
CHANGED
oscura/optimization/parallel.py
CHANGED
|
@@ -14,12 +14,15 @@ from concurrent.futures import (
|
|
|
14
14
|
as_completed,
|
|
15
15
|
)
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import TYPE_CHECKING, Any
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
20
|
|
|
21
21
|
from oscura.core.exceptions import AnalysisError
|
|
22
22
|
|
|
23
|
+
T = TypeVar("T")
|
|
24
|
+
R = TypeVar("R")
|
|
25
|
+
|
|
23
26
|
if TYPE_CHECKING:
|
|
24
27
|
from collections.abc import Iterable
|
|
25
28
|
|
|
@@ -29,7 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
@dataclass
|
|
32
|
-
class ParallelResult[R]:
|
|
35
|
+
class ParallelResult(Generic[R]):
|
|
33
36
|
"""Result from parallel execution.
|
|
34
37
|
|
|
35
38
|
Attributes:
|
|
@@ -93,7 +96,7 @@ def get_optimal_workers(max_workers: int | None = None) -> int:
|
|
|
93
96
|
return min(max_workers, cpu_count)
|
|
94
97
|
|
|
95
98
|
|
|
96
|
-
def parallel_map
|
|
99
|
+
def parallel_map(
|
|
97
100
|
func: Callable[[T], R],
|
|
98
101
|
iterable: Iterable[T],
|
|
99
102
|
*,
|
|
@@ -171,7 +174,7 @@ def parallel_map[T, R](
|
|
|
171
174
|
)
|
|
172
175
|
|
|
173
176
|
|
|
174
|
-
def parallel_reduce
|
|
177
|
+
def parallel_reduce(
|
|
175
178
|
func: Callable[[T], R],
|
|
176
179
|
iterable: Iterable[T],
|
|
177
180
|
reducer: Callable[[list[R]], Any],
|
|
@@ -219,7 +222,7 @@ def parallel_reduce[T, R](
|
|
|
219
222
|
return reducer(result.results)
|
|
220
223
|
|
|
221
224
|
|
|
222
|
-
def batch_parallel_map
|
|
225
|
+
def batch_parallel_map(
|
|
223
226
|
func: Callable[[list[T]], list[R]],
|
|
224
227
|
iterable: Iterable[T],
|
|
225
228
|
*,
|
|
@@ -297,7 +300,7 @@ def batch_parallel_map[T, R](
|
|
|
297
300
|
)
|
|
298
301
|
|
|
299
302
|
|
|
300
|
-
def parallel_filter
|
|
303
|
+
def parallel_filter(
|
|
301
304
|
func: Callable[[T], bool],
|
|
302
305
|
iterable: Iterable[T],
|
|
303
306
|
*,
|
oscura/pipeline/composition.py
CHANGED
|
@@ -4,8 +4,6 @@ This module implements compose() and pipe() functions for functional-style
|
|
|
4
4
|
trace processing, with support for operator overloading.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
from collections.abc import Callable
|
|
10
8
|
from functools import reduce, wraps
|
|
11
9
|
from typing import Any, TypeVar
|
oscura/plugins/cli.py
CHANGED
oscura/reporting/comparison.py
CHANGED
oscura/reporting/config.py
CHANGED
|
@@ -594,7 +594,7 @@ def get_available_analyses(input_type: InputType) -> list[AnalysisDomain]:
|
|
|
594
594
|
|
|
595
595
|
|
|
596
596
|
# Type alias for progress callbacks
|
|
597
|
-
ProgressCallback = Callable[[ProgressInfo], None]
|
|
597
|
+
ProgressCallback = Callable[["ProgressInfo"], None]
|
|
598
598
|
|
|
599
599
|
|
|
600
600
|
__all__ = [
|
oscura/reporting/output.py
CHANGED
|
@@ -4,8 +4,6 @@ This module provides directory structure and file management for analysis
|
|
|
4
4
|
report outputs, including plots, JSON/YAML data exports, and logs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
7
|
import json
|
|
10
8
|
from datetime import datetime
|
|
11
9
|
from pathlib import Path
|
|
@@ -14,7 +12,7 @@ from typing import Any
|
|
|
14
12
|
import numpy as np
|
|
15
13
|
import yaml
|
|
16
14
|
|
|
17
|
-
from oscura.reporting.config import AnalysisDomain
|
|
15
|
+
from oscura.reporting.config import AnalysisDomain
|
|
18
16
|
|
|
19
17
|
|
|
20
18
|
def _sanitize_for_serialization(obj: Any, max_depth: int = 10) -> Any:
|
oscura/reporting/sections.py
CHANGED