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.
Files changed (91) hide show
  1. oscura/__init__.py +1 -1
  2. oscura/analyzers/packet/payload_extraction.py +2 -4
  3. oscura/analyzers/packet/stream.py +5 -5
  4. oscura/analyzers/patterns/__init__.py +4 -3
  5. oscura/analyzers/patterns/clustering.py +3 -1
  6. oscura/analyzers/power/ac_power.py +0 -2
  7. oscura/analyzers/power/basic.py +0 -2
  8. oscura/analyzers/power/ripple.py +0 -2
  9. oscura/analyzers/signal_integrity/embedding.py +0 -2
  10. oscura/analyzers/signal_integrity/sparams.py +28 -206
  11. oscura/analyzers/spectral/fft.py +0 -2
  12. oscura/analyzers/statistical/__init__.py +3 -3
  13. oscura/analyzers/statistical/checksum.py +2 -0
  14. oscura/analyzers/statistical/classification.py +2 -0
  15. oscura/analyzers/statistical/entropy.py +11 -9
  16. oscura/analyzers/statistical/ngrams.py +4 -2
  17. oscura/api/fluent.py +2 -2
  18. oscura/automotive/__init__.py +1 -3
  19. oscura/automotive/can/__init__.py +0 -2
  20. oscura/automotive/dbc/__init__.py +0 -2
  21. oscura/automotive/dtc/__init__.py +0 -2
  22. oscura/automotive/dtc/data.json +2763 -0
  23. oscura/automotive/dtc/database.py +37 -2769
  24. oscura/automotive/j1939/__init__.py +0 -2
  25. oscura/automotive/loaders/__init__.py +0 -2
  26. oscura/automotive/loaders/asc.py +0 -2
  27. oscura/automotive/loaders/blf.py +0 -2
  28. oscura/automotive/loaders/csv_can.py +0 -2
  29. oscura/automotive/obd/__init__.py +0 -2
  30. oscura/automotive/uds/__init__.py +0 -2
  31. oscura/automotive/uds/models.py +0 -2
  32. oscura/cli/main.py +0 -2
  33. oscura/cli/shell.py +0 -2
  34. oscura/config/loader.py +0 -2
  35. oscura/core/backend_selector.py +1 -1
  36. oscura/core/correlation.py +0 -2
  37. oscura/core/exceptions.py +56 -2
  38. oscura/core/lazy.py +5 -3
  39. oscura/core/memory_limits.py +0 -2
  40. oscura/core/numba_backend.py +5 -7
  41. oscura/core/uncertainty.py +3 -3
  42. oscura/dsl/interpreter.py +2 -0
  43. oscura/dsl/parser.py +8 -6
  44. oscura/exploratory/error_recovery.py +3 -3
  45. oscura/exploratory/parse.py +2 -0
  46. oscura/exploratory/recovery.py +2 -0
  47. oscura/exploratory/sync.py +2 -0
  48. oscura/export/wireshark/generator.py +1 -1
  49. oscura/export/wireshark/type_mapping.py +2 -0
  50. oscura/exporters/hdf5.py +1 -3
  51. oscura/extensibility/templates.py +0 -8
  52. oscura/inference/active_learning/lstar.py +2 -4
  53. oscura/inference/active_learning/observation_table.py +0 -2
  54. oscura/inference/active_learning/oracle.py +3 -1
  55. oscura/inference/active_learning/teachers/simulator.py +1 -3
  56. oscura/inference/alignment.py +2 -0
  57. oscura/inference/message_format.py +2 -0
  58. oscura/inference/protocol_dsl.py +7 -5
  59. oscura/inference/sequences.py +12 -14
  60. oscura/inference/state_machine.py +2 -0
  61. oscura/integrations/llm.py +3 -1
  62. oscura/jupyter/display.py +0 -2
  63. oscura/loaders/__init__.py +67 -51
  64. oscura/loaders/pcap.py +1 -1
  65. oscura/loaders/touchstone.py +221 -0
  66. oscura/math/arithmetic.py +0 -2
  67. oscura/optimization/parallel.py +9 -6
  68. oscura/pipeline/composition.py +0 -2
  69. oscura/plugins/cli.py +0 -2
  70. oscura/reporting/comparison.py +0 -2
  71. oscura/reporting/config.py +1 -1
  72. oscura/reporting/formatting/emphasis.py +2 -0
  73. oscura/reporting/formatting/numbers.py +0 -2
  74. oscura/reporting/output.py +1 -3
  75. oscura/reporting/sections.py +0 -2
  76. oscura/search/anomaly.py +2 -0
  77. oscura/session/session.py +80 -13
  78. oscura/testing/synthetic.py +2 -0
  79. oscura/ui/formatters.py +4 -2
  80. oscura/utils/buffer.py +2 -2
  81. oscura/utils/lazy.py +5 -5
  82. oscura/utils/memory_advanced.py +2 -2
  83. oscura/utils/memory_extensions.py +2 -2
  84. oscura/visualization/colors.py +0 -2
  85. oscura/visualization/power.py +2 -0
  86. oscura/workflows/multi_trace.py +2 -0
  87. {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/METADATA +37 -16
  88. {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/RECORD +91 -89
  89. {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/WHEEL +0 -0
  90. {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/entry_points.txt +0 -0
  91. {oscura-0.1.2.dist-info → oscura-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
 
@@ -12,6 +12,8 @@ Key capabilities:
12
12
  - Export to NetworkX graph for analysis
13
13
  """
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from copy import deepcopy
16
18
  from dataclasses import dataclass
17
19
  from typing import Any
@@ -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
- ) -> "FailoverLLMClient":
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
@@ -12,8 +12,6 @@ Example:
12
12
  In [2]: display_trace(trace) # Shows rich HTML summary
13
13
  """
14
14
 
15
- from __future__ import annotations
16
-
17
15
  from typing import Any
18
16
 
19
17
  try:
@@ -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
- raise UnsupportedFormatError(
229
- loader_name,
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
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
27
27
 
28
28
  # Try to import dpkt for full PCAP support
29
29
  try:
30
- import dpkt # type: ignore[import-untyped]
30
+ import dpkt # type: ignore[import-not-found]
31
31
 
32
32
  DPKT_AVAILABLE = True
33
33
  except ImportError:
@@ -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
@@ -14,8 +14,6 @@ References:
14
14
  IEEE 181-2011: Standard for Transitional Waveform Definitions
15
15
  """
16
16
 
17
- from __future__ import annotations
18
-
19
17
  import ast
20
18
  import operator
21
19
  from collections.abc import Callable
@@ -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[T, R](
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[T, R](
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[T, R](
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[T](
303
+ def parallel_filter(
301
304
  func: Callable[[T], bool],
302
305
  iterable: Iterable[T],
303
306
  *,
@@ -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
@@ -4,8 +4,6 @@ This module provides command-line interface for plugin management including
4
4
  list, info, enable/disable, install, and validate operations.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import hashlib
10
8
  import logging
11
9
  import shutil
@@ -9,8 +9,6 @@ Example:
9
9
  >>> report = generate_comparison_report(baseline, current, "comparison.pdf")
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from typing import Any, Literal
15
13
 
16
14
  from oscura.reporting.core import Report, ReportConfig, Section
@@ -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__ = [
@@ -3,6 +3,8 @@
3
3
  Simple text formatting for terminal output.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  from enum import Enum
7
9
 
8
10
 
@@ -4,8 +4,6 @@ Provides comprehensive number formatting with SI prefixes, engineering notation,
4
4
  locale support, and specification comparison capabilities.
5
5
  """
6
6
 
7
- from __future__ import annotations
8
-
9
7
  import math
10
8
  from datetime import datetime
11
9
  from typing import ClassVar
@@ -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 # noqa: TC001
15
+ from oscura.reporting.config import AnalysisDomain
18
16
 
19
17
 
20
18
  def _sanitize_for_serialization(obj: Any, max_depth: int = 10) -> Any:
@@ -9,8 +9,6 @@ Example:
9
9
  >>> section = create_title_section("Signal Analysis Report", author="Engineer")
10
10
  """
11
11
 
12
- from __future__ import annotations
13
-
14
12
  from datetime import datetime
15
13
  from typing import Any
16
14
 
oscura/search/anomaly.py CHANGED
@@ -4,6 +4,8 @@ This module provides automated detection of glitches, timing violations,
4
4
  and protocol errors with context extraction for debugging.
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
7
9
  from typing import Any
8
10
 
9
11
  import numpy as np