Source code for Instruments_Libraries.M8070B

"""
Created on Mon Jul  21 18:56:32 2025

@author: Maxim Weizel
"""

from typing import Any

import numpy as np

from .BaseInstrument import BaseInstrument

# Try to import matlab engine
try:
    import matlab

    MATLAB_AVAILABLE = True
except (ImportError, ModuleNotFoundError) as e:
    MATLAB_AVAILABLE = False
    print("!" * 80)
    print("WARNING: MATLAB Engine for Python is not installed or not working correctly.")
    print("""Generic functions will work, but calling IQTools-related functions will 
    raise a RuntimeError.""")
    print("To install, use pip with the version matching your MATLAB installation:")
    print("  - R2024b: pip install matlabengine==24.2.*")
    print("  - R2025a: pip install matlabengine==25.1.*")
    print("  - R2025b: pip install matlabengine==25.2.*")
    print("  - Other : pip install matlabengine==<MATLAB_VERSION>.<MATLAB_VERSION_MINOR>.*")
    print(f"Detailed Error: {e}")
    print("!" * 80)


[docs] class M8070B(BaseInstrument): """ Start the M8070B Software. Go to Utilities-> SCPI Server Information. Copy the VISA resource string (usually localhost). """ def __init__( self, resource_str="TCPIP0::localhost::hislip0::INSTR", visa_library="@py", **kwargs ): kwargs.setdefault("write_termination", "\n") kwargs.setdefault("timeout", 2000) # 2s super().__init__(resource_str, visa_library=visa_library, **kwargs) self._channelLS = [1, 2] # print(self.get_idn()) # ============================================================================= # Check functions # =============================================================================
[docs] def validate_channel(self, channel: int) -> int: channel = int(channel) if channel not in self._channelLS: raise ValueError("Channel must be 1 or 2") return channel
# ============================================================================= # M8199B - Get Values and Modes # =============================================================================
[docs] def get_amplitude(self, channel: int = 1) -> float: """Returns the differential amplitude setting for the selected channel in Volts. Parameters ---------- channel : int, optional 1 or 2, by default 1 """ channel = self.validate_channel(channel) return float(self.query(f":SOURce:VOLTage:AMPLitude? 'M2.DataOut{channel}'"))
[docs] def get_output_state(self, channel: int) -> int: """Returns the output state (0 or 1) for the selected channel. Parameters ---------- channel : int Channel 1 or 2 """ channel = self.validate_channel(channel) return int(self.query(f":OUTPut:STATe? 'M2.DataOut{channel}'"))
[docs] def get_delay(self, channel: int) -> float: """Returns the delay for the selected channel in seconds. Parameters ---------- channel : int Channel 1 or 2 """ channel = self.validate_channel(channel) return float(self.query(f":ARM:DELay? 'M2.DataOut{channel}'"))
# ============================================================================= # M8199B - Set Values and Modes # =============================================================================
[docs] def set_amplitude(self, channel: int, amplitude: int | float) -> None: """Differential amplitude setting for the selected channel. Parameters ---------- channel : int Channel 1 or 2 amplitude : int/float Amplitude setting in V. Must be between 0.1 and 2.7 V """ channel = self.validate_channel(channel) if 0.1 <= amplitude <= 2.7: self.write(f":SOURce:VOLTage:AMPLitude 'M2.DataOut{channel}', {amplitude}") else: raise ValueError(f"Value must be between 0.1 and 2.7 V. You entered: {amplitude} V")
[docs] def set_rf_power(self, channel: int, power_dBm: int | float) -> None: # noqa: N803 """Sets the Signal Generator Output Power in dBm. Converts from dBm to V and uses ``set_amplitude()`` internally. Parameters ---------- channel : int Channel 1 or 2 power_dBm : int/float Output Power in dBm """ power_watt = 10 ** (power_dBm / 10) * 1e-3 v_rms = (50 * power_watt) ** 0.5 # 50 Ohm System amplitude = v_rms * np.sqrt(2) self.set_amplitude(channel, amplitude)
[docs] def set_output_power_level(self, channel: int, power_dBm: int | float) -> None: # noqa: N803 """Sets the Signal Generator Output Power in dBm. Converts from dBm to V and uses ``set_amplitude()`` internally. Alias for set_rf_power(). Parameters ---------- channel : int Channel 1 or 2 value : int/float Output Power in dBm """ self.set_rf_power(channel, power_dBm)
[docs] def set_output(self, channel: int, state: int | str) -> None: """Activate or deactivate the selected channel output. Parameters ---------- channel : int Channel 1 or 2 state : int | str One of: 0, 1, "off", "on" Raises ------ ValueError Channel must be 1 or 2. State must be 0 or 1. """ channel = self.validate_channel(channel) state_normalized = self._parse_state(state) self.write(f":OUTPut:STATe 'M2.DataOut{channel}', {state_normalized}")
[docs] def set_rf_output(self, channel: int, state: int | str) -> None: """Activate or deactivate the selected channel output. Alias for set_output(). """ self.set_output(channel, state)
[docs] def set_delay(self, channel: int, delay: float) -> None: """Set the delay for the selected channel in seconds. Parameters ---------- channel : int Channel 1 or 2 delay : float Delay in seconds. """ channel = self.validate_channel(channel) if not (-25e-9 <= delay <= 25e-9): raise ValueError(f"Delay must be between -25 and 25ns. You entered: {delay} s") self.write(f":ARM:DELay 'M2.DataOut{channel}',{delay}")
# ============================================================================= # M8008A Clock Module - Get Values and Modes # =============================================================================
[docs] def get_sample_clk_out_frequency(self, channel: int = 1) -> float: """Returns the sample clock OUT1 or OUT2 frequency from the M8008A CLK module. Both frequencies are the same (in Hz). Parameters ---------- channel : int, optional 1 or 2, by default 1 """ channel = self.validate_channel(channel) return float(self.query(f":OUTPut:FREQuency? 'M1.SampleClkOut{channel}'"))
[docs] def get_sample_clk_out2_state(self) -> int: """Returns the sample clock OUT2 state (0 or 1) from the M8008A CLK module. Sample clock OUT1 cannot be turned off. """ return int(self.query(":OUTPut:STATe? 'M1.SampleClkOut2'"))
[docs] def get_sample_clk_out2_power(self) -> float: """Returns the sample clock OUT2 Power in dBm from the M8008A CLK module. Sample clock OUT1 cannot be influenced. """ return float(self.query(":OUTPut:POWer? 'M1.SampleClkOut2'"))
# ============================================================================= # M8008A Clock Module - Set Values and Modes # =============================================================================
[docs] def set_sample_clk_out2_state(self, state: int | str) -> None: """Sets the sample clock OUT2 state from the M8008A CLK module. Sample clock OUT1 cannot be turned off. Parameters ---------- state : int | str One of: 0, 1, "off", "on" """ state_normalized = self._parse_state(state) self.write(f":OUTPut:STATe 'M1.SampleClkOut2', {state_normalized}")
[docs] def set_sample_clk_out2_power(self, power: int | float) -> None: """Sets the sample clock OUT2 Power in dBm from the M8008A CLK module. Sample clock OUT1 cannot be influenced. Parameters ---------- power : int | float Sample Clock OUT2 Power in dBm. Must be between -5 and 12 dBm """ if not (-5 <= power <= 12): raise ValueError(f"Power must be between -5 and 12 dBm. You entered: {power} dBm") self.write(f":SOURce:POWer 'M1.SampleClkOut2',{power}")
# ============================================================================= # M8199B Calling IQTools Functions # =============================================================================
[docs] def set_freq_cw( self, matlab_engine, channel: int, frequency: float, correction: int = 0, run: int = 1, fs: float = 256e9, ) -> None: """Set the CW tone frequency on the AWG via MATLAB engine. Parameters ---------- matlab_engine : matlab.engine An active MATLAB engine session. channel : int AWG channel (1 or 2). frequency : float Tone frequency in Hz. correction : int, optional Enable correction (default 0). run : int, optional AWG run number (default 1). fs : float, optional AWG sample rate (default 256e9). """ # 1) Validate channel channel = self.validate_channel(channel) if not MATLAB_AVAILABLE: raise RuntimeError( "MATLAB Engine is not installed. Cannot use IQTools-related " "functions. Please check the startup warning for installation " "instructions." ) # 2) Define constants # magnitude is zeros(1,1) in MATLAB; make it a 1×1 double magnitude = matlab.double([[0]]) # in dB # fmt: off # 3) Build channelMapping # MATLAB expects numeric arrays, not raw Python lists if channel == 1: py_map = [[1, 0], [0, 0]] else: py_map = [[0, 0], [1, 0]] channel_mapping = matlab.double(py_map) # 4) Call iqtone to generate the IQ vector # We ask for 5 outputs so that the last one is chMap. iqdata, _, _, _, chMap = matlab_engine.iqtone( # noqa: N806 'sampleRate', fs, 'numSamples', 0, 'tone', frequency, 'phase', 'Random', 'normalize', 1, 'magnitude', magnitude, 'correction', correction, 'channelMapping', channel_mapping, nargout=5 ) # 6) Push the generated IQ out to the AWG matlab_engine.iqdownload( iqdata, fs, 'channelMapping', chMap, 'segmentNumber', 1, 'run', run, nargout=0 )
# fmt: on
[docs] def iqdownload( self, matlab_engine, iqdata, fs: float, *, segment_number: int = 1, normalize: bool = True, channel_mapping=None, sequence=None, marker=None, arb_config=None, keep_open: bool = False, run: bool = True, segment_length=None, segment_offset=None, lo_amplitude=None, lo_f_center=None, segm_name=None, rms=None, ) -> Any: """ Download a pre-generated IQ waveform to the AWG. Parameters ---------- matlab_engine : matlab.engine Active MATLAB engine session. iqdata : array-like Real or complex samples (each column = one waveform). Can be empty for a connection check. fs : float Sample rate in Hz. segment_number : int, optional Which segment to download into (default=1). normalize : bool, optional Auto-scale to DAC range (default=True). channel_mapping : array-like, optional 2xM logical matrix mapping IQ data columns to AWG channels. sequence : any, optional Sequence table descriptor. marker : array-like of int, optional Marker bits per sample. arb_config : struct, optional AWG configuration struct (default from arbConfig file). keep_open : bool, optional If True, leave connection open after download (default=False). run : bool, optional If True, start AWG immediately after download (default=True). segment_length, segment_offset, lo_amplitude, lo_f_center, segm_name, rms : Other advanced options as per MATLAB doc. Returns ------- result The output of the MATLAB `iqdownload` call (empty or status). """ # fmt: off if not MATLAB_AVAILABLE: raise RuntimeError( "MATLAB Engine is not installed. Cannot use IQTools-related " "functions. Please check the startup warning for installation " "instructions." ) # Build the var/val list args = [ 'segmentNumber', int(segment_number), 'normalize', int(normalize), 'keepOpen', int(keep_open), 'run', int(run) ] # fmt: on if channel_mapping is not None: args += ["channelMapping", channel_mapping] if sequence is not None: args += ["sequence", sequence] if marker is not None: args += ["marker", marker] if arb_config is not None: args += ["arbConfig", arb_config] if segment_length is not None: args += ["segmentLength", segment_length] if segment_offset is not None: args += ["segmentOffset", segment_offset] if lo_amplitude is not None: args += ["loAmplitude", lo_amplitude] if lo_f_center is not None: args += ["loFcenter", lo_f_center] if segm_name is not None: args += ["segmName", segm_name] if rms is not None: args += ["rms", rms] # Call MATLAB result = matlab_engine.iqdownload(iqdata, fs, *args, nargout=1) return result
[docs] def generate_multitone( self, matlab_engine, *, channel: int, tones: np.ndarray, magnitudes_dBm: np.ndarray | None = None, # noqa: N803 phases: np.ndarray | str = "Random", correction: int = 0, run: int = 1, fs: float = 256e9, ) -> None: """Set the CW tone frequency on the AWG via MATLAB engine. Parameters ---------- matlab_engine : matlab.engine An active MATLAB engine session. channel : int AWG channel (1 or 2). tones : ndarray Tone frequency in Hz. magnitudes_dBm : ndarray, optional Tone magnitude in dBm (default None). correction : int, optional Enable correction (default 0). run : int, optional AWG run number (default 1). fs : float, optional AWG sample rate (default 256e9). """ # 1) Validate channel channel = self.validate_channel(channel) if not MATLAB_AVAILABLE: raise RuntimeError( "MATLAB Engine is not installed. Cannot use IQTools-related " "functions. Please check the startup warning for installation " "instructions." ) # 2) Prepare arrays frequency = np.asarray(tones, dtype=np.float64) # 1-D if magnitudes_dBm is None: magnitudes_dBm = np.zeros_like(frequency, dtype=np.float64) # dBm # noqa: N806 else: magnitudes_dBm = np.asarray(magnitudes_dBm, dtype=np.float64) # noqa: N806 # phase: either the literal 'Random' or a numeric vector if isinstance(phases, str): phase_arg = phases # pass plain string to MATLAB else: phase_arg = np.asarray(phases, dtype=np.float64) # make column vectors if iqtone expects columns # phase_arg = matlab.double([[v] for v in phase_arr]) # If iqtone wants column vectors for tone/magnitude too: # tone_arg = matlab.double([[v] for v in frequency]) # magnitude_arg = matlab.double([[v] for v in magnitudes_dB]) # If iqtone wants row vectors for tone/magnitude too: tone_arg = frequency magnitude_arg = magnitudes_dBm # channelMapping already fine channel_mapping = matlab.double([[1, 0], [0, 0]] if channel == 1 else [[0, 0], [1, 0]]) iqdata, _, _, _, chMap = matlab_engine.iqtone( # noqa: N806 "sampleRate", fs, "numSamples", 0, "tone", tone_arg, # explicit column vector "phase", phase_arg, # string or column vector "normalize", 1, "magnitude", magnitude_arg, # explicit column vector "correction", correction, "channelMapping", channel_mapping, nargout=5, ) # 6) Push the generated IQ out to the AWG matlab_engine.iqdownload( iqdata, fs, "channelMapping", chMap, "segmentNumber", 1, "run", run, nargout=0 )