pytestlab 0.2.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.
- pytestlab/__init__.py +69 -0
- pytestlab/_log.py +62 -0
- pytestlab/analysis/__init__.py +7 -0
- pytestlab/analysis/fft.py +91 -0
- pytestlab/analysis/fr_analysis.py +38 -0
- pytestlab/bench.py +618 -0
- pytestlab/cli.py +916 -0
- pytestlab/common/__init__.py +5 -0
- pytestlab/common/enums.py +89 -0
- pytestlab/common/health.py +19 -0
- pytestlab/compliance/__init__.py +19 -0
- pytestlab/compliance/audit.py +88 -0
- pytestlab/compliance/patch.py +120 -0
- pytestlab/compliance/signature.py +146 -0
- pytestlab/compliance/tsa.py +51 -0
- pytestlab/config/__init__.py +16 -0
- pytestlab/config/_mixins.py +37 -0
- pytestlab/config/accuracy.py +62 -0
- pytestlab/config/base.py +21 -0
- pytestlab/config/bench_config.py +80 -0
- pytestlab/config/bench_loader.py +36 -0
- pytestlab/config/config.py +20 -0
- pytestlab/config/dc_active_load_config.py +98 -0
- pytestlab/config/instrument_config.py +31 -0
- pytestlab/config/loader.py +179 -0
- pytestlab/config/multimeter_config.py +145 -0
- pytestlab/config/oscilloscope_config.py +63 -0
- pytestlab/config/power_meter_config.py +14 -0
- pytestlab/config/power_supply_config.py +78 -0
- pytestlab/config/spectrum_analyzer_config.py +18 -0
- pytestlab/config/virtual_instrument_config.py +7 -0
- pytestlab/config/vna_config.py +16 -0
- pytestlab/config/waveform_generator_config.py +38 -0
- pytestlab/errors.py +145 -0
- pytestlab/experiments/__init__.py +6 -0
- pytestlab/experiments/database.py +724 -0
- pytestlab/experiments/experiments.py +164 -0
- pytestlab/experiments/results.py +357 -0
- pytestlab/experiments/sweep.py +658 -0
- pytestlab/gui/__init__.py +23 -0
- pytestlab/gui/async_utils.py +63 -0
- pytestlab/gui/builder.py +209 -0
- pytestlab/instruments/AutoInstrument.py +565 -0
- pytestlab/instruments/DCActiveLoad.py +361 -0
- pytestlab/instruments/Multimeter.py +309 -0
- pytestlab/instruments/Oscilloscope.py +1643 -0
- pytestlab/instruments/PowerMeter.py +86 -0
- pytestlab/instruments/PowerSupply.py +539 -0
- pytestlab/instruments/SpectrumAnalyser.py +55 -0
- pytestlab/instruments/VectorNetworkAnalyser.py +72 -0
- pytestlab/instruments/VirtualInstrument.py +79 -0
- pytestlab/instruments/WaveformGenerator.py +1704 -0
- pytestlab/instruments/__init__.py +8 -0
- pytestlab/instruments/backends/__init__.py +6 -0
- pytestlab/instruments/backends/async_visa_backend.py +167 -0
- pytestlab/instruments/backends/lamb.py +189 -0
- pytestlab/instruments/backends/recording_backend.py +110 -0
- pytestlab/instruments/backends/replay_backend.py +162 -0
- pytestlab/instruments/backends/session_recording_backend.py +148 -0
- pytestlab/instruments/backends/sim_backend.py +658 -0
- pytestlab/instruments/backends/visa_backend.py +134 -0
- pytestlab/instruments/instrument.py +670 -0
- pytestlab/instruments/scpi_engine.py +481 -0
- pytestlab/measurements/__init__.py +8 -0
- pytestlab/measurements/session.py +365 -0
- pytestlab/profiles/__init__.py +0 -0
- pytestlab/profiles/keysight/34460A.yaml +27 -0
- pytestlab/profiles/keysight/34470A.yaml +30 -0
- pytestlab/profiles/keysight/DSOX1202G.yaml +102 -0
- pytestlab/profiles/keysight/DSOX1204G.yaml +132 -0
- pytestlab/profiles/keysight/DSOX3054G.yaml +122 -0
- pytestlab/profiles/keysight/E36313A.yaml +37 -0
- pytestlab/profiles/keysight/E5071C_VNA.yaml +13 -0
- pytestlab/profiles/keysight/EDU33212A.yaml +53 -0
- pytestlab/profiles/keysight/EDU34450A.yaml +176 -0
- pytestlab/profiles/keysight/EDU36311A.yaml +122 -0
- pytestlab/profiles/keysight/EDU36311A_recorded.yaml +48 -0
- pytestlab/profiles/keysight/EL33133A.yaml +126 -0
- pytestlab/profiles/keysight/HD304MSO.yaml +148 -0
- pytestlab/profiles/keysight/MSOX2024A.yaml +122 -0
- pytestlab/profiles/keysight/MXR404A.yaml +102 -0
- pytestlab/profiles/keysight/N9000A_SA.yaml +12 -0
- pytestlab/profiles/keysight/U2000A_PM.yaml +10 -0
- pytestlab/profiles/pytestlab/binary_wave_data.bin +1 -0
- pytestlab/profiles/pytestlab/virtual_instrument.yaml +68 -0
- pytestlab/profiles/rohdeschwarz/NGE102B.yaml +144 -0
- pytestlab/schemas/awg.json +166 -0
- pytestlab/schemas/dc_active_load.json +511 -0
- pytestlab/schemas/dmm.json +120 -0
- pytestlab/schemas/oscilloscope.json +316 -0
- pytestlab/schemas/psu.json +86 -0
- pytestlab-0.2.1.dist-info/METADATA +317 -0
- pytestlab-0.2.1.dist-info/RECORD +95 -0
- pytestlab-0.2.1.dist-info/WHEEL +4 -0
- pytestlab-0.2.1.dist-info/entry_points.txt +2 -0
pytestlab/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pytestlab – scientific measurement toolbox
|
|
3
|
+
=========================================
|
|
4
|
+
|
|
5
|
+
This file now **re-exports** the new high-level measurement builder so that
|
|
6
|
+
users can simply write
|
|
7
|
+
|
|
8
|
+
>>> from pytestlab import Measurement
|
|
9
|
+
|
|
10
|
+
or
|
|
11
|
+
|
|
12
|
+
>>> from pytestlab.measurements import Measurement
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.1" # Update this line to change the version
|
|
16
|
+
|
|
17
|
+
from importlib import metadata as _metadata
|
|
18
|
+
import logging # Required for set_log_level
|
|
19
|
+
from ._log import get_logger, set_log_level, reinitialize_logging
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ─── Public re-exports from existing sub-packages ──────────────────────────
|
|
24
|
+
from .config import *
|
|
25
|
+
from .experiments import *
|
|
26
|
+
from .instruments import *
|
|
27
|
+
from .errors import *
|
|
28
|
+
from .bench import Bench
|
|
29
|
+
|
|
30
|
+
# ─── New high-level builder ────────────────────────────────────────────────
|
|
31
|
+
from .measurements.session import Measurement, MeasurementSession # noqa: E402 pylint: disable=wrong-import-position
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Config
|
|
35
|
+
"OscilloscopeConfig",
|
|
36
|
+
"MultimeterConfig",
|
|
37
|
+
"PowerSupplyConfig",
|
|
38
|
+
"WaveformGeneratorConfig",
|
|
39
|
+
# Instruments
|
|
40
|
+
"Oscilloscope",
|
|
41
|
+
"Multimeter",
|
|
42
|
+
"PowerSupply",
|
|
43
|
+
"WaveformGenerator",
|
|
44
|
+
"AutoInstrument",
|
|
45
|
+
"InstrumentManager",
|
|
46
|
+
# Experiments
|
|
47
|
+
"Experiment",
|
|
48
|
+
"MeasurementResult",
|
|
49
|
+
# Errors
|
|
50
|
+
"InstrumentError",
|
|
51
|
+
"InstrumentConfigurationError",
|
|
52
|
+
"InstrumentParameterError",
|
|
53
|
+
# Bench System
|
|
54
|
+
"Bench",
|
|
55
|
+
# New measurement system
|
|
56
|
+
"Measurement",
|
|
57
|
+
"MeasurementSession",
|
|
58
|
+
"set_log_level",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Version is defined statically above, but we can still try to get it from metadata
|
|
62
|
+
# try: # pragma: no cover
|
|
63
|
+
# __version__ = _metadata.version(__name__)
|
|
64
|
+
# except _metadata.PackageNotFoundError: # pragma: no cover
|
|
65
|
+
# __version__ = "0.1.0"
|
|
66
|
+
|
|
67
|
+
# needs to be imported after the MeasurementResult class is defined
|
|
68
|
+
from . import compliance
|
|
69
|
+
compliance.initialize()
|
pytestlab/_log.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
_root_logger = logging.getLogger("pytestlab")
|
|
7
|
+
|
|
8
|
+
def setup_logging():
|
|
9
|
+
"""
|
|
10
|
+
Set up logging for pytestlab.
|
|
11
|
+
This function is called automatically when the first logger is requested.
|
|
12
|
+
It can also be called manually to reconfigure logging.
|
|
13
|
+
"""
|
|
14
|
+
level_name = os.getenv("PYTESTLAB_LOG_LEVEL", os.getenv("PYTESTLAB_LOG", "WARNING")).upper()
|
|
15
|
+
log_level = getattr(logging, level_name, logging.WARNING)
|
|
16
|
+
log_file = os.getenv("PYTESTLAB_LOG_FILE")
|
|
17
|
+
|
|
18
|
+
# Remove all handlers from our logger to reconfigure
|
|
19
|
+
for handler in _root_logger.handlers[:]:
|
|
20
|
+
_root_logger.removeHandler(handler)
|
|
21
|
+
|
|
22
|
+
if log_file:
|
|
23
|
+
handler: logging.Handler = logging.FileHandler(log_file)
|
|
24
|
+
else:
|
|
25
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
26
|
+
|
|
27
|
+
formatter = logging.Formatter("%(asctime)s %(name)s %(levelname)s – %(message)s")
|
|
28
|
+
handler.setFormatter(formatter)
|
|
29
|
+
_root_logger.addHandler(handler)
|
|
30
|
+
_root_logger.setLevel(log_level)
|
|
31
|
+
# Enable propagation so pytest caplog can capture logs
|
|
32
|
+
_root_logger.propagate = True
|
|
33
|
+
|
|
34
|
+
def reinitialize_logging():
|
|
35
|
+
"""
|
|
36
|
+
Reinitialize logging configuration, useful for tests that modify environment variables.
|
|
37
|
+
"""
|
|
38
|
+
setup_logging()
|
|
39
|
+
|
|
40
|
+
def set_log_level(level: int | str):
|
|
41
|
+
"""
|
|
42
|
+
Sets the logging level for the pytestlab logger.
|
|
43
|
+
:param level: The logging level, e.g., "DEBUG", "INFO", logging.DEBUG, logging.INFO.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
if isinstance(level, str):
|
|
47
|
+
level = level.upper()
|
|
48
|
+
_root_logger.setLevel(level)
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
# Log the warning and keep the current level
|
|
51
|
+
_root_logger.warning(f"Invalid log level: {level}")
|
|
52
|
+
|
|
53
|
+
def get_logger(name: str) -> logging.Logger:
|
|
54
|
+
"""
|
|
55
|
+
Retrieves a logger instance, configuring the root logger on first call.
|
|
56
|
+
"""
|
|
57
|
+
if not _root_logger.handlers:
|
|
58
|
+
setup_logging()
|
|
59
|
+
return _root_logger.getChild(name)
|
|
60
|
+
|
|
61
|
+
# Initial setup when module is loaded.
|
|
62
|
+
setup_logging()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# pytestlab/analysis/fft.py
|
|
2
|
+
import numpy as np
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
|
|
5
|
+
def compute_fft(
|
|
6
|
+
time_array: np.ndarray,
|
|
7
|
+
voltage_array: np.ndarray,
|
|
8
|
+
window: Optional[str] = 'hann' # Example: allow windowing
|
|
9
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
10
|
+
"""
|
|
11
|
+
Computes the Fast Fourier Transform (FFT) of a given time-domain signal.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
time_array: NumPy array of time values.
|
|
15
|
+
voltage_array: NumPy array of voltage (or other signal) values.
|
|
16
|
+
window: Optional windowing function to apply before FFT (e.g., 'hann', 'hamming').
|
|
17
|
+
Set to None or an empty string to disable windowing.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
A tuple containing:
|
|
21
|
+
- frequency_array: NumPy array of frequency bins.
|
|
22
|
+
- magnitude_array: NumPy array of FFT magnitudes (linear).
|
|
23
|
+
"""
|
|
24
|
+
# Ensure time_array and voltage_array are of the same size
|
|
25
|
+
if not isinstance(time_array, np.ndarray) or not isinstance(voltage_array, np.ndarray):
|
|
26
|
+
raise TypeError("Input arrays must be NumPy ndarrays.")
|
|
27
|
+
if time_array.size != voltage_array.size:
|
|
28
|
+
raise ValueError("Time and voltage arrays must have the same size.")
|
|
29
|
+
if time_array.ndim != 1 or voltage_array.ndim != 1:
|
|
30
|
+
raise ValueError("Input arrays must be 1-dimensional.")
|
|
31
|
+
|
|
32
|
+
N = voltage_array.size
|
|
33
|
+
if N == 0:
|
|
34
|
+
return np.array([]), np.array([])
|
|
35
|
+
if N <= 1: # FFT not meaningful for 0 or 1 sample
|
|
36
|
+
return np.array([]), np.array([])
|
|
37
|
+
|
|
38
|
+
# Apply windowing if specified
|
|
39
|
+
if window:
|
|
40
|
+
if window == 'hann':
|
|
41
|
+
voltage_array_windowed = voltage_array * np.hanning(N)
|
|
42
|
+
elif window == 'hamming':
|
|
43
|
+
voltage_array_windowed = voltage_array * np.hamming(N)
|
|
44
|
+
# Add other windows or raise error for unsupported window
|
|
45
|
+
# elif window is None or window == '': # No window
|
|
46
|
+
# voltage_array_windowed = voltage_array
|
|
47
|
+
else:
|
|
48
|
+
raise ValueError(f"Unsupported window function: {window}. Supported: 'hann', 'hamming', None.")
|
|
49
|
+
else: # No window
|
|
50
|
+
voltage_array_windowed = voltage_array
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Compute FFT
|
|
54
|
+
fft_values = np.fft.fft(voltage_array_windowed)
|
|
55
|
+
# Take only positive frequencies (first N // 2 points)
|
|
56
|
+
# For real inputs, the FFT is symmetric, so we only need half.
|
|
57
|
+
fft_magnitudes = np.abs(fft_values)[:N // 2]
|
|
58
|
+
|
|
59
|
+
# Compute frequency bins
|
|
60
|
+
# This requires sampling frequency Fs.
|
|
61
|
+
# A more robust way if time_array is uniformly spaced:
|
|
62
|
+
if N > 1 and (time_array[-1] - time_array[0]) > 0:
|
|
63
|
+
# Total duration T = time_array[-1] - time_array[0]
|
|
64
|
+
# Number of sampling intervals = N - 1
|
|
65
|
+
# Sampling interval dt = T / (N - 1)
|
|
66
|
+
# Sampling frequency Fs = 1 / dt = (N - 1) / T
|
|
67
|
+
dt = (time_array[-1] - time_array[0]) / (N - 1)
|
|
68
|
+
if dt <= 0: # Avoid division by zero or negative dt if time_array is not monotonic
|
|
69
|
+
# This case should ideally be caught by pre-checks or handled by requiring Fs
|
|
70
|
+
return np.array([]), np.array([])
|
|
71
|
+
Fs = 1 / dt
|
|
72
|
+
elif N == 1 and time_array.size == 1: # Single point, Fs is undefined, Nyquist is 0
|
|
73
|
+
# Return empty or a specific representation for a single point "spectrum"
|
|
74
|
+
# For FFT, typically need >1 points.
|
|
75
|
+
# Or, if Fs is *known* (e.g. from instrument settings), it could be passed in.
|
|
76
|
+
# For now, aligning with N<=1 check above.
|
|
77
|
+
return np.array([]), np.array([])
|
|
78
|
+
else: # Cannot determine Fs (e.g., N > 1 but time_array[-1] == time_array[0]), or N is too small
|
|
79
|
+
# Or if time_array is not sorted, (time_array[-1] - time_array[0]) could be non-positive.
|
|
80
|
+
# Consider requiring Fs as an input for more robustness if time_array properties are not guaranteed.
|
|
81
|
+
# For now, returning empty as a safe default.
|
|
82
|
+
return np.array([]), np.array([])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# d = sampling interval = 1/Fs
|
|
86
|
+
frequency_array = np.fft.fftfreq(N, d=1/Fs)[:N // 2]
|
|
87
|
+
|
|
88
|
+
# fft_magnitudes are linear. User can convert to dB if needed:
|
|
89
|
+
# fft_magnitudes_db = 20 * np.log10(fft_magnitudes)
|
|
90
|
+
|
|
91
|
+
return frequency_array, fft_magnitudes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# pytestlab/analysis/fr_analysis.py
|
|
2
|
+
# Placeholder for Frequency Response Analysis functions.
|
|
3
|
+
|
|
4
|
+
# Example conceptual function (not implemented yet):
|
|
5
|
+
# import numpy as np
|
|
6
|
+
# from typing import Tuple
|
|
7
|
+
#
|
|
8
|
+
# def compute_frequency_response(
|
|
9
|
+
# input_time_array: np.ndarray,
|
|
10
|
+
# input_voltage_array: np.ndarray,
|
|
11
|
+
# output_time_array: np.ndarray,
|
|
12
|
+
# output_voltage_array: np.ndarray,
|
|
13
|
+
# window: Optional[str] = 'hann'
|
|
14
|
+
# ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
15
|
+
# """
|
|
16
|
+
# Computes the frequency response (e.g., gain and phase) from input and output signals.
|
|
17
|
+
#
|
|
18
|
+
# Args:
|
|
19
|
+
# input_time_array: NumPy array of time values for the input signal.
|
|
20
|
+
# input_voltage_array: NumPy array of voltage values for the input signal.
|
|
21
|
+
# output_time_array: NumPy array of time values for the output signal.
|
|
22
|
+
# output_voltage_array: NumPy array of voltage values for the output signal.
|
|
23
|
+
# window: Optional windowing function to apply before FFT.
|
|
24
|
+
#
|
|
25
|
+
# Returns:
|
|
26
|
+
# A tuple containing:
|
|
27
|
+
# - frequency_array: NumPy array of frequency bins.
|
|
28
|
+
# - gain_array: NumPy array of gain values (e.g., in dB).
|
|
29
|
+
# - phase_array: NumPy array of phase values (e.g., in degrees or radians).
|
|
30
|
+
# """
|
|
31
|
+
# # This would involve:
|
|
32
|
+
# # 1. Aligning or ensuring consistent sampling of input and output signals.
|
|
33
|
+
# # 2. Computing FFT of both input and output signals (e.g., using compute_fft from .fft).
|
|
34
|
+
# # 3. Calculating the transfer function H(f) = FFT(output) / FFT(input).
|
|
35
|
+
# # 4. Deriving gain (magnitude of H(f)) and phase (angle of H(f)).
|
|
36
|
+
# pass
|
|
37
|
+
|
|
38
|
+
__all__ = [] # No functions exported yet
|