class PNABase(VisaInstrument): """ Base qcodes driver for Agilent/Keysight series PNAs http://na.support.keysight.com/pna/help/latest/Programming/GP-IB_Command_Finder/SCPI_Command_Tree.htm Note: Currently this driver only expects a single channel on the PNA. We can handle multiple traces, but using traces across multiple channels may have unexpected results. """ def __init__( self, name: str, address: str, # Set frequency ranges min_freq: Union[int, float], max_freq: Union[int, float], # Set power ranges min_power: Union[int, float], max_power: Union[int, float], nports: int, # Number of ports on the PNA **kwargs: Any) -> None: super().__init__(name, address, terminator='\n', **kwargs) self.min_freq = min_freq self.max_freq = max_freq #Ports ports = ChannelList(self, "PNAPorts", PNAPort) for port_num in range(1, nports + 1): port = PNAPort(self, f"port{port_num}", port_num, min_power, max_power) ports.append(port) self.add_submodule(f"port{port_num}", port) ports.lock() self.add_submodule("ports", ports) # Drive power self.add_parameter('power', label='Power', get_cmd='SOUR:POW?', get_parser=float, set_cmd='SOUR:POW {:.2f}', unit='dBm', vals=Numbers(min_value=min_power, max_value=max_power)) # IF bandwidth self.add_parameter('if_bandwidth', label='IF Bandwidth', get_cmd='SENS:BAND?', get_parser=float, set_cmd='SENS:BAND {:.2f}', unit='Hz', vals=Numbers(min_value=1, max_value=15e6)) # Number of averages (also resets averages) self.add_parameter('averages_enabled', label='Averages Enabled', get_cmd="SENS:AVER?", set_cmd="SENS:AVER {}", val_mapping={ True: '1', False: '0' }) self.add_parameter('averages', label='Averages', get_cmd='SENS:AVER:COUN?', get_parser=int, set_cmd='SENS:AVER:COUN {:d}', unit='', vals=Numbers(min_value=1, max_value=65536)) # Setting frequency range self.add_parameter('start', label='Start Frequency', get_cmd='SENS:FREQ:STAR?', get_parser=float, set_cmd='SENS:FREQ:STAR {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('stop', label='Stop Frequency', get_cmd='SENS:FREQ:STOP?', get_parser=float, set_cmd='SENS:FREQ:STOP {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('center', label='Center Frequency', get_cmd='SENS:FREQ:CENT?', get_parser=float, set_cmd='SENS:FREQ:CENT {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('span', label='Frequency Span', get_cmd='SENS:FREQ:SPAN?', get_parser=float, set_cmd='SENS:FREQ:SPAN {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) # Number of points in a sweep self.add_parameter('points', label='Points', get_cmd='SENS:SWE:POIN?', get_parser=int, set_cmd='SENS:SWE:POIN {}', unit='', vals=Numbers(min_value=1, max_value=100001)) # Electrical delay self.add_parameter('electrical_delay', label='Electrical Delay', get_cmd='CALC:CORR:EDEL:TIME?', get_parser=float, set_cmd='CALC:CORR:EDEL:TIME {:.6e}', unit='s', vals=Numbers(min_value=0, max_value=100000)) # Sweep Time self.add_parameter('sweep_time', label='Time', get_cmd='SENS:SWE:TIME?', get_parser=float, unit='s', vals=Numbers(0, 1e6)) # Sweep Mode self.add_parameter('sweep_mode', label='Mode', get_cmd='SENS:SWE:MODE?', set_cmd='SENS:SWE:MODE {}', vals=Enum("HOLD", "CONT", "GRO", "SING")) # Group trigger count self.add_parameter('group_trigger_count', get_cmd="SENS:SWE:GRO:COUN?", get_parser=int, set_cmd="SENS:SWE:GRO:COUN {}", vals=Ints(1, 2000000)) # Trigger Source self.add_parameter('trigger_source', get_cmd="TRIG:SOUR?", set_cmd="TRIG:SOUR {}", vals=Enum("EXT", "IMM", "MAN")) # Traces self.add_parameter('active_trace', label='Active Trace', get_cmd="CALC:PAR:MNUM?", get_parser=int, set_cmd="CALC:PAR:MNUM {}", vals=Numbers(min_value=1, max_value=24)) # Note: Traces will be accessed through the traces property which # updates the channellist to include only active trace numbers self._traces = ChannelList(self, "PNATraces", PNATrace) self.add_submodule("traces", self._traces) # Add shortcuts to first trace trace1 = self.traces[0] for param in trace1.parameters.values(): self.parameters[param.name] = param # And also add a link to run sweep self.run_sweep = trace1.run_sweep # Set this trace to be the default (it's possible to end up in a # situation where no traces are selected, causing parameter snapshots # to fail) self.active_trace(trace1.trace_num) # Set auto_sweep parameter # If we want to return multiple traces per setpoint without sweeping # multiple times, we should set this to false self.add_parameter('auto_sweep', label='Auto Sweep', set_cmd=None, get_cmd=None, vals=Bool(), initial_value=True) # A default output format on initialisation self.write('FORM REAL,32') self.write('FORM:BORD NORM') self.connect_message() @property def traces(self) -> ChannelList: """ Update channel list with active traces and return the new list """ # Keep track of which trace was active before. This command may fail # if no traces were selected. try: active_trace = self.active_trace() except VisaIOError as e: if e.error_code == errors.StatusCode.error_timeout: active_trace = None else: raise # Get a list of traces from the instrument and fill in the traces list parlist = self.get_trace_catalog().split(",") self._traces.clear() for trace_name in parlist[::2]: trace_num = self.select_trace_by_name(trace_name) pna_trace = PNATrace(self, "tr{}".format(trace_num), trace_name, trace_num) self._traces.append(pna_trace) # Restore the active trace if there was one if active_trace: self.active_trace(active_trace) # Return the list of traces on the instrument return self._traces def get_options(self) -> Sequence[str]: # Query the instrument for what options are installed return self.ask('*OPT?').strip('"').split(',') def get_trace_catalog(self): """ Get the trace catalog, that is a list of trace and sweep types from the PNA. The format of the returned trace is: trace_name,trace_type,trace_name,trace_type... """ return self.ask("CALC:PAR:CAT:EXT?").strip('"') def select_trace_by_name(self, trace_name: str) -> int: """ Select a trace on the PNA by name. Returns: The trace number of the selected trace """ self.write(f"CALC:PAR:SEL '{trace_name}'") return self.active_trace() def reset_averages(self): """ Reset averaging """ self.write("SENS:AVER:CLE") def averages_on(self): """ Turn on trace averaging """ self.averages_enabled(True) def averages_off(self): """ Turn off trace averaging """ self.averages_enabled(False) def _set_power_limits(self, min_power: Union[int, float], max_power: Union[int, float]) -> None: """ Set port power limits """ self.power.vals = Numbers(min_value=min_power, max_value=max_power) for port in self.ports: port._set_power_limits(min_power, max_power)
class VNABase(VisaInstrument): """ Note: Currently this driver only expects a single channel on the VNA. We can handle multiple traces, but using traces across multiple channels may have unexpected results. """ def __init__( self, name: str, address: str, # Set frequency ranges min_freq: Union[int, float], max_freq: Union[int, float], # Set power ranges min_power: Union[int, float], max_power: Union[int, float], nports: int, # Number of ports on the VNA **kwargs: Any) -> None: super().__init__(name, address, terminator='\n', **kwargs) self.min_freq = min_freq self.max_freq = max_freq #Ports ports = ChannelList(self, "VNAPorts", VNAPort) for port_num in range(1, nports + 1): port = VNAPort(self, f"port{port_num}", port_num, min_power, max_power) ports.append(port) self.add_submodule(f"port{port_num}", port) ports.lock() self.add_submodule("ports", ports) # Drive power#only low and high self.add_parameter('power', label='Power', get_cmd=':SOUR:POW:PORT?', get_parser=float, set_parser=float, set_cmd='SOUR:POW:PORT {:.2f}', unit='dBm', vals=Numbers(-30, 30)) """ self.add_parameter(name='power', label='Power', unit='dBm', get_cmd=':SENS:FSEGM:POW:PORT1?', set_cmd=':SENS:FSEGM:POW:PORT1 {:.4f}', get_parser=float, set_parser=float, vals=Numbers(-30, 30)) """ self.add_parameter(name='frequencyList', label='Frequency list', unit='Hz', get_cmd=':SENS:FREQ:DATA?', set_cmd=':SENS:FREQ:DATA {:.4f}', get_parser=float, set_parser=float, vals=Numbers(300e3, 20e9)) # IF bandwidth self.add_parameter(name='if_bandwidth', label='Intermediate Frequency Bandwidth', unit='Hz', get_cmd=':SENS:BWID?', set_cmd=':SENS:BWID {}', get_parser=float, set_parser=float, vals=Enum(10, 20, 30, 50, 70, 100, 200, 300, 500, 700, 1000, 3000, 5000, 7000, 10000)) """ # Number of averages (also resets averages) self.add_parameter('averages_enabled', label='Averages Enabled', get_cmd="SENS:AVER?", set_cmd="SENS:AVER {}", val_mapping={True: '1', False: '0'}) self.add_parameter('averages', label='Averages', get_cmd='SENS:AVER:COUN?', get_parser=int, set_cmd='SENS:AVER:COUN {:d}', unit='', vals=Numbers(min_value=1, max_value=65536)) """ # Setting frequency range self.add_parameter(name='start', label='Start frequency', unit='Hz', get_cmd=':SENS:FREQ:STAR?', set_cmd=':SENS:FREQ:STAR {:.4f}', get_parser=float, set_parser=float, vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter(name='stop', label='Stop frequency', unit='Hz', get_cmd=':SENS:FREQ:STOP?', set_cmd=':SENS:FREQ:STOP {:.4f}', get_parser=float, set_parser=float, vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter(name='center', label='Center frequency', unit='Hz', get_cmd=':SENS:FREQ:CENT?', set_cmd=':SENS:FREQ:CENT {:.4f}', get_parser=float, set_parser=float, vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter(name='span', label='Frequency span', unit='Hz', get_cmd=':SENS:FREQ:SPAN?', set_cmd=':SENS:FREQ:SPAN {:.4f}', get_parser=float, set_parser=float, vals=Numbers(min_value=100, max_value=20e9)) self.add_parameter( name='groupdelay', label='group delay in reference plane subsystem', unit='s', get_cmd=':SENS:CORR:EXT:PORT1?', set_cmd=':SENS:CORR:EXT:PORT1 {:.4f}', get_parser=float, set_parser=float, ) # Number of points in a sweep self.add_parameter(name='points', label='Number of measurement points', unit='', get_cmd=':SENS:SWE:POIN?', set_cmd=':SENS:SWE:POIN {}', get_parser=int, set_parser=int, vals=Numbers(2, 20001)) # Sweep Time self.add_parameter('sweep_time', label='Time', get_cmd='SENS:SWE:TIME?', get_parser=float, unit='s', vals=Numbers(0, 1e6)) # Sweep Mode self.add_parameter(name='sweep_mode', label='Hold_Function', get_cmd=':SENS:HOLD:FUNC?', set_cmd=':SENS:HOLD:FUNC {}', get_parser=str, vals=Enum('HOLD', 'hold', 'CONT', 'continuous', 'SING', 'single')) """ # Group trigger count self.add_parameter('group_trigger_count', get_cmd="SENS:SWE:GRO:COUN?", get_parser=int, set_cmd="SENS:SWE:GRO:COUN {}", vals=Ints(1, 2000000)) # Trigger Source self.add_parameter('trigger_source', get_cmd="TRIG:SOUR?", set_cmd="TRIG:SOUR {}", vals=Enum("EXT", "IMM", "MAN")) """ # Traces self.add_parameter('active_trace', label='Active Trace', get_cmd="CALC1:PAR:SEL?", get_parser=int, set_cmd="CALC1:PAR{}:SEL") """ self.add_parameter('active_trace', label='Active Trace', get_cmd="CALC1:PAR:MNUM?", get_parser=int, set_cmd="CALC1:PAR:MNUM {}", vals=Numbers(min_value=1, max_value=24)) """ self.add_parameter('Number_Traces', label='Number Traces', get_cmd="CALC1:PAR:COUN?", get_parser=int, set_cmd="CALC1:PAR:COUN {}") # Note: Traces will be accessed through the traces property which # updates the channellist to include only active trace numbers self._traces = ChannelList(self, "VNATraces", VNATrace) self.add_submodule("traces", self._traces) #print(self._traces) #ChannelList.__getitem__(self, 0) # Add shortcuts to first trace trace1 = self.traces[0] for param in trace1.parameters.values(): self.parameters[param.name] = param #print(param) # And also add a link to run sweep print('in sweep') self.run_sweep = trace1.run_sweep # Set this trace to be the default (it's possible to end up in a # situation where no traces are selected, causing parameter snapshots # to fail) self.active_trace(trace1.trace_num) """ for i in range(2): trace1 = self.traces[i] for param in trace1.parameters.values(): self.parameters[param.name] = param print(param) # And also add a link to run sweep self.run_sweep = trace1.run_sweep # Set this trace to be the default (it's possible to end up in a # situation where no traces are selected, causing parameter snapshots # to fail) self.active_trace(trace1.trace_num) """ # Set this trace to be the default (it's possible to end up in a # situation where no traces are selected, causing parameter snapshots # to fail) # Set auto_sweep parameter # If we want to return multiple traces per setpoint without sweeping # multiple times, we should set this to false self.add_parameter('auto_sweep', label='Auto Sweep', set_cmd=None, get_cmd=None, vals=Bool(), initial_value=False) self.connect_message() @property def traces(self) -> ChannelList: """ Update channel list with active traces and return the new list """ # Keep track of which trace was active before. This command may fail # if no traces were selected. # try: # active_trace = self.active_trace() # except VisaIOError as e: # if e.error_code == errors.StatusCode.error_timeout: # active_trace = None # else: # raise # Get a list of traces from the instrument and fill in the traces list num_of_traces = self.Number_Traces.get() # self_traces.clear() may cause problems when channellist empty self._traces.clear() for trace_num in range(1, num_of_traces + 1): vna_trace = VNATrace(self, "tr{}".format(trace_num), str(trace_num), trace_num) self._traces.append(vna_trace) # # Restore the active trace if there was one # if active_trace: # self.active_trace(active_trace) # Return the list of traces on the instrument return self._traces """ def get_options(self) -> List[str]: # Query the instrument for what options are installed return self.ask('*OPT?').strip('"').split(',') """ """ def get_trace_catalog(self): Get the trace catalog, that is a list of trace and sweep types from the VNA. The format of the returned trace is: trace_name,trace_type,trace_name,trace_type... num=self.Number_Traces.get() return self.ask("CALC:PAR:CAT:EXT?").strip('"') """ """
class SwitchChannel(InstrumentChannel): """ This class represents one input or output port of a switch instrument. Args: instrument: the instrument to which this port belongs to name: name or alias of this port in the parent instrument's ChannelList raw_name: name of this port in the driver's channel table, as given by ``self._session.get_channel_name`` """ def __init__(self, instrument: NI_Switch, name: str, raw_name: str): super().__init__(instrument, name) self._session = self.root_instrument.session self.raw_name = raw_name self.connection_list = ChannelList(self.root_instrument, "connections", type(self), snapshotable=False) self.add_parameter( "connections", docstring="The value of this read-only parameter " "is a list of the names of the channels " "to which this channel is connected to.", get_cmd=self._read_connections, set_cmd=False, ) def _update_connection_list(self) -> None: self.connection_list.clear() for ch in self.root_instrument.channels: if ch is self: continue status = self._session.can_connect(self.raw_name, ch.raw_name) if status == PathCapability.PATH_EXISTS: self.connection_list.append(ch) def _read_connections(self) -> List[str]: r""" Returns a list of the channels to which this channel is connected to. """ self._update_connection_list() return [ch.short_name for ch in self.connection_list] def connect_to(self, other: "InstrumentChannel") -> None: """ Connect this channel to another channel. If either of the channels is already connected to something else, disconnect both channels first. If the channels are already connected to each other, do nothing. If the two channels cannot be connected, raises a ``DriverError``, see the ``niswitch.Session.connect`` documentation for further details. """ self.root_instrument.channels.get_validator().validate(other) status = self._session.can_connect(self.raw_name, other.raw_name) if status == PathCapability.PATH_EXISTS: # already connected, do nothing return elif status == PathCapability.PATH_AVAILABLE: # not connected pass elif status == PathCapability.RESOURCE_IN_USE: # connected to something else self.disconnect_from_all() other.disconnect_from_all() self._session.connect(self.raw_name, other.raw_name) self.connection_list.append(other) other.connection_list.append(self) def disconnect_from(self, other: "InstrumentChannel") -> None: """ Disconnect this channel from another chanel. If the channels are not connected, raises a ``DriverError``. """ self.root_instrument.channels.get_validator().validate(other) self._session.disconnect(self.raw_name, other.raw_name) other.connection_list.remove(self) self.connection_list.remove(other) def disconnect_from_all(self) -> None: """ Disconnect this channel from all channels it is connected to. """ while len(self.connection_list) > 0: ch = cast(InstrumentChannel, self.connection_list[0]) self.disconnect_from(ch)
class CMTBase(VisaInstrument): """ Base qcodes driver for CMT Network Analyzers """ def __init__(self, name: str, address: str, # Set frequency ranges min_freq: Union[int, float], max_freq: Union[int, float], # Set power ranges min_power: Union[int, float], max_power: Union[int, float], nports: int, # Number of ports on the CMT **kwargs: Any) -> None: super().__init__(name, address, terminator='\n', **kwargs) self.min_freq = min_freq self.max_freq = max_freq # set the active trace to 1 since we can't figure out how to read it out self.select_trace_by_name( "tr1" ) #Ports ports = ChannelList(self, "CMTPorts", CMTPort) for port_num in range(1, nports+1): port = CMTPort(self, f"port{port_num}", port_num, min_power, max_power) ports.append(port) self.add_submodule(f"port{port_num}", port) ports.lock() self.add_submodule("ports", ports) # Drive power self.add_parameter('power', label='$P_{\mathrm{VNA}}$', get_cmd='SOUR:POW?', get_parser=float, set_cmd='SOUR:POW {:.2f}', unit='dBm', vals=Numbers(min_value=min_power, max_value=max_power)) # IF bandwidth self.add_parameter('if_bandwidth', label='IF Bandwidth', get_cmd='SENS:BAND?', get_parser=float, set_cmd='SENS:BAND {:.2f}', unit='Hz', vals=Numbers(min_value=1, max_value=15e6)) # Number of averages (also resets averages) self.add_parameter('averages_enabled', label='Averages Enabled', get_cmd="SENS:AVER?", set_cmd="SENS:AVER {}", val_mapping={True: '1', False: '0'}) self.add_parameter('averages', label='Averages', get_cmd='SENS:AVER:COUN?', get_parser=int, set_cmd='SENS:AVER:COUN {:d}', unit='', vals=Numbers(min_value=1, max_value=65536)) # RF OUT -> Turns the VNA ON/OFF self.add_parameter('rf_out', label='RF Out', get_cmd="OUTP:STAT?", set_cmd="OUTP:STAT {}", val_mapping={True: '1', False: '0'}) # Setting frequency range self.add_parameter('start', label='Start Frequency', get_cmd='SENS:FREQ:STAR?', get_parser=float, set_cmd='SENS:FREQ:STAR {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('stop', label='Stop Frequency', get_cmd='SENS:FREQ:STOP?', get_parser=float, set_cmd='SENS:FREQ:STOP {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('center', label='Center Frequency', get_cmd='SENS:FREQ:CENT?', get_parser=float, set_cmd='SENS:FREQ:CENT {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('span', label='Frequency Span', get_cmd='SENS:FREQ:SPAN?', get_parser=float, set_cmd='SENS:FREQ:SPAN {}', unit='Hz', vals=Numbers(min_value=0, max_value=max_freq)) # Number of points in a sweep self.add_parameter('points', label='Points', get_cmd='SENS:SWE:POIN?', get_parser=int, set_cmd='SENS:SWE:POIN {}', unit='', vals=Numbers(min_value=1, max_value=200001)) # Electrical delay self.add_parameter('electrical_delay', label='Electrical Delay', get_cmd='CALC:CORR:EDEL:TIME?', get_parser=float, set_cmd='CALC:CORR:EDEL:TIME {:.6e}', unit='s', vals=Numbers(min_value=0, max_value=100000)) # Sweep Time # SYST:CYCL:TIME:MEAS? self.add_parameter('sweep_time', label='Time', get_cmd='SYST:CYCL:TIME:MEAS?', get_parser=float, unit='s', vals=Numbers(0, 1e6)) # Sweep Mode self.add_parameter('sweep_mode', label='Mode', get_cmd='INIT:CONT?', set_cmd='INIT:CONT {}', vals=Ints( 0, 1 )) # Number of traces in the channel # TODO: this shoudl probably be moved to port self.add_parameter('trace_count', get_cmd="CALC:PAR:COUN?", get_parser=int, set_cmd="SENS:PAR:COUN {}", vals=Ints(1, 2000000)) # Trigger Source self.add_parameter('trigger_source', get_cmd="TRIG:SOUR?", set_cmd="TRIG:SOUR {}", vals=Enum("EXT", "IMM", "MAN")) # Traces self.add_parameter('active_trace', label='Active Trace', get_parser=int, set_cmd="CALC:PAR{}:SEL", vals=Numbers(min_value=1, max_value=24)) self.active_trace.get = lambda : self._active_trace # Note: Traces will be accessed through the traces property which # updates the channellist to include only active trace numbers self._traces = ChannelList(self, "CMTTraces", CMTTrace) self.add_submodule("traces", self._traces) # Add shortcuts to first trace trace1 = self.traces[0] params = trace1.parameters if not isinstance(params, dict): raise RuntimeError(f"Expected trace.parameters to be a dict got " f"{type(params)}") for param in params.values(): self.parameters[param.name] = param # And also add a link to run sweep self.run_sweep = trace1.run_sweep # Set this trace to be the default (it's possible to end up in a # situation where no traces are selected, causing parameter snapshots # to fail) self.active_trace(trace1.trace_num) # Set auto_sweep parameter # If we want to return multiple traces per setpoint without sweeping # multiple times, we should set this to false self.add_parameter('auto_sweep', label='Auto Sweep', set_cmd=None, get_cmd=None, vals=Bool(), initial_value=True) # A default output format on initialisation self.write('FORM REAL,32') self.write('FORM:BORD NORM') self.connect_message() def _set_wait( self, w ): ''' set if we should wait for full trace to acquire. this should only be set through the wait parameter ''' self._wait = w @property def traces(self) -> ChannelList: """ Update channel list with active traces and return the new list """ # Keep track of which trace was active before. This command may fail # if no traces were selected. try: active_trace = self.active_trace() except VisaIOError as e: if e.error_code == errors.StatusCode.error_timeout: active_trace = None else: raise # Get a list of traces from the instrument and fill in the traces list parlist = self.get_trace_catalog().split(",") self._traces.clear() for trace_name in parlist[::2]: trace_num = self.select_trace_by_name(trace_name) CMT_trace = CMTTrace(self, "tr{}".format(trace_num), trace_name, trace_num) self._traces.append(CMT_trace) # Restore the active trace if there was one if active_trace: self.active_trace(active_trace) # Return the list of traces on the instrument return self._traces def get_options(self) -> Sequence[str]: # Query the instrument for what options are installed return self.ask('*OPT?').strip('"').split(',') def trigger_trace( self ) : ''' trigger a single trace and wait for it to be finished ''' self.write('TRIG:SOUR BUS') self.write('TRIG:SEQ:SING') #Trigger a single sweep self.ask('*OPC?') #Wait for measurement to complete def trigger_internal( self ) : ''' go back to internal triggering ''' self.write('TRIG:SOUR INT') def get_trace_catalog(self): """ Get the trace catalog, that is a list of trace and sweep types from the CMT. The format of the returned trace is: trace_name,trace_type,trace_name,trace_type... we will use tr1_sxx,sxx,tr2_sxx_sxx,... """ catalog = "" for n in range( self.trace_count() ): query = f"CALC:PAR{n+1}:DEF?" s = self.ask(query).strip('"') catalog += f"tr{n+1}_{s},{s}," return catalog[:-1] def select_trace_by_name(self, trace_name: str) -> int: """ Select a trace on the CMT by name. Returns: The trace number of the selected trace """ tr_num = int( trace_name[2] ) self.write(f"CALC:PAR{tr_num}:SEL") self._active_trace = tr_num return tr_num def reset_averages(self): """ Reset averaging """ self.write("SENS:AVER:CLE") def averages_on(self): """ Turn on trace averaging """ self.averages_enabled(True) def averages_off(self): """ Turn off trace averaging """ self.averages_enabled(False) def _set_power_limits(self, min_power: Union[int, float], max_power: Union[int, float]) -> None: """ Set port power limits """ self.power.vals = Numbers(min_value=min_power, max_value=max_power) for port in self.ports: port._set_power_limits(min_power, max_power)
class PNABase(VisaInstrument): """ Base qcodes driver for Agilent/Keysight series PNAs http://na.support.keysight.com/pna/help/latest/Programming/GP-IB_Command_Finder/SCPI_Command_Tree.htm Note: Currently this driver only expects a single channel on the PNA. We can handle multiple traces, but using traces across multiple channels may have unexpected results. """ def __init__( self, name: str, address: str, min_freq: Union[int, float], max_freq: Union[int, float], # Set frequency ranges min_power: Union[int, float], max_power: Union[int, float], # Set power ranges nports: int, # Number of ports on the PNA **kwargs: Any) -> None: super().__init__(name, address, terminator='\n', **kwargs) #Ports ports = ChannelList(self, "PNAPorts", PNAPort) for port_num in range(1, nports + 1): port = PNAPort(self, f"port{port_num}", port_num, min_power, max_power) ports.append(port) self.add_submodule(f"port{port_num}", port) ports.lock() self.add_submodule("ports", ports) # Drive power self.add_parameter('power', label='Power', get_cmd='SOUR:POW?', get_parser=float, set_cmd='SOUR:POW {:.2f}', unit='dBm', vals=Numbers(min_value=min_power, max_value=max_power)) # IF bandwidth self.add_parameter('if_bandwidth', label='IF Bandwidth', get_cmd='SENS:BAND?', get_parser=float, set_cmd='SENS:BAND {:.2f}', unit='Hz', vals=Numbers(min_value=1, max_value=15e6)) # Number of averages (also resets averages) self.add_parameter('averages_enabled', label='Averages Enabled', get_cmd="SENS:AVER?", set_cmd="SENS:AVER {}", val_mapping={ True: '1', False: '0' }) self.add_parameter('averages', label='Averages', get_cmd='SENS:AVER:COUN?', get_parser=int, set_cmd='SENS:AVER:COUN {:d}', unit='', vals=Numbers(min_value=1, max_value=65536)) # Setting frequency range self.add_parameter('start', label='Start Frequency', get_cmd='SENS:FREQ:STAR?', get_parser=float, set_cmd='SENS:FREQ:STAR {}', unit='', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('stop', label='Stop Frequency', get_cmd='SENS:FREQ:STOP?', get_parser=float, set_cmd='SENS:FREQ:STOP {}', unit='', vals=Numbers(min_value=min_freq, max_value=max_freq)) # Number of points in a sweep self.add_parameter('points', label='Points', get_cmd='SENS:SWE:POIN?', get_parser=int, set_cmd='SENS:SWE:POIN {}', unit='', vals=Numbers(min_value=1, max_value=100001)) # Electrical delay self.add_parameter('electrical_delay', label='Electrical Delay', get_cmd='CALC:CORR:EDEL:TIME?', get_parser=float, set_cmd='CALC:CORR:EDEL:TIME {:.6e}', unit='s', vals=Numbers(min_value=0, max_value=100000)) # Sweep Time self.add_parameter('sweep_time', label='Time', get_cmd='SENS:SWE:TIME?', get_parser=float, unit='s', vals=Numbers(0, 1e6)) # Sweep Mode self.add_parameter('sweep_mode', label='Mode', get_cmd='SENS:SWE:MODE?', set_cmd='SENS:SWE:MODE {}', vals=Enum("HOLD", "CONT", "GRO", "SING")) # Traces self.add_parameter('active_trace', label='Active Trace', get_cmd="CALC:PAR:MNUM?", get_parser=int, set_cmd="CALC:PAR:MNUM {}", vals=Numbers(min_value=1, max_value=24)) # Note: Traces will be accessed through the traces property which updates # the channellist to include only active trace numbers self._traces = ChannelList(self, "PNATraces", PNATrace) self.add_submodule("traces", self._traces) # Add shortcuts to trace 1 trace1 = PNATrace(self, "tr1", 1) for param in trace1.parameters.values(): self.parameters[param.name] = param # Set this trace to be the default (it's possible to end up in a situation where # no traces are selected, causing parameter snapshots to fail) self.active_trace(1) # Set auto_sweep parameter # If we want to return multiple traces per setpoint without sweeping # multiple times, we should set this to false self.add_parameter('auto_sweep', label='Auto Sweep', set_cmd=None, get_cmd=None, vals=Bool(), initial_value=True) # A default output format on initialisation self.write('FORM REAL,32') self.write('FORM:BORD NORM') self.connect_message() @property def traces(self) -> ChannelList: """ Update channel list with active traces and return the new list """ parlist = self.ask("CALC:PAR:CAT:EXT?").strip('"').split(",") self._traces.clear() for trace in parlist[::2]: trnum = PNATrace.parse_paramstring(trace)[2] pna_trace = PNATrace(self, "tr{}".format(trnum), int(trnum)) self._traces.append(pna_trace) return self._traces def get_options(self) -> Sequence[str]: # Query the instrument for what options are installed return self.ask('*OPT?').strip('"').split(',') def reset_averages(self): """ Reset averaging """ self.write("SENS:AVER:CLE") def averages_on(self): """ Turn on trace averaging """ self.averages_enabled(True) def averages_off(self): """ Turn off trace averaging """ self.averages_enabled(False) def _set_auto_sweep(self, val: bool) -> None: self._auto_sweep = val def _set_power_limits(self, min_power: Union[int, float], max_power: Union[int, float]) -> None: """ Set port power limits """ self.power.vals = Numbers(min_value=min_power, max_value=max_power) for port in self.ports: port._set_power_limits(min_power, max_power)
class PNABase(VisaInstrument): """ Base qcodes driver for Agilent/Keysight series PNAs http://na.support.keysight.com/pna/help/latest/Programming/GP-IB_Command_Finder/SCPI_Command_Tree.htm Note: Currently this driver only expects a single channel on the PNA. We can handle multiple traces, but using traces across multiple channels may have unexpected results. """ def __init__(self, name: str, address: str, **kwargs: Any) -> None: super().__init__(name, address, terminator='\n', **kwargs) min_freq = 300e3 max_freq = 20e9 min_power = -85 max_power = 10 nports = 2 # # #Ports # ports = ChannelList(self, "PNAPorts", PNAPort) # self.add_submodule("ports", ports) # for port_num in range(1, nports+1): # port = PNAPort(self, f"port{port_num}", port_num, # min_power, max_power) # ports.append(port) ## self.add_submodule(f"port{port_num}", port) # ports.lock() # Drive power self.add_parameter('power', label='Power', get_cmd='SOUR:POW?', get_parser=float, set_cmd='SOUR:POW {:.2f}', unit='dBm', vals=Numbers(min_value=min_power, max_value=max_power)) # IF bandwidth self.add_parameter( 'if_bandwidth', label='IF Bandwidth', get_cmd='SENS:BAND?', get_parser=float, set_cmd='SENS:BAND {:.2f}', unit='Hz', #vals=Numbers(min_value=10, max_value=15e6)) vals=Enum(*np.append( [10**6, 15 * 10**5], np.kron([10, 15, 20, 30, 50, 70], 10**np.arange(5))))) # Number of averages (also resets averages) self.add_parameter('averages_enabled', label='Averages Enabled', get_cmd="SENS:AVER?", set_cmd="SENS:AVER {}", val_mapping={ True: '1', False: '0' }) self.add_parameter('averages', label='Averages', get_cmd='SENS:AVER:COUN?', get_parser=int, set_cmd='SENS:AVER:COUN {:d}', unit='', vals=Numbers(min_value=1, max_value=999)) self.add_parameter('average_trigger', label='Average Trigger', get_cmd=':TRIG:AVER?', set_cmd=':TRIG:AVER {}', vals=Enum('on', 'On', 'ON', 'off', 'Off', 'OFF')) # Setting frequency range self.add_parameter('start', label='Start Frequency', get_cmd='SENS:FREQ:STAR?', get_parser=float, set_cmd='SENS:FREQ:STAR {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('stop', label='Stop Frequency', get_cmd='SENS:FREQ:STOP?', get_parser=float, set_cmd='SENS:FREQ:STOP {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('center', label='Center Frequency', get_cmd='SENS:FREQ:CENT?', get_parser=float, set_cmd='SENS:FREQ:CENT {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) self.add_parameter('span', label='Frequency Span', get_cmd='SENS:FREQ:SPAN?', get_parser=float, set_cmd='SENS:FREQ:SPAN {}', unit='Hz', vals=Numbers(min_value=min_freq, max_value=max_freq)) # Number of points in a sweep self.add_parameter('points', label='Points', get_cmd='SENS:SWE:POIN?', get_parser=int, set_cmd='SENS:SWE:POIN {}', unit='', vals=Numbers(min_value=1, max_value=20001)) # Electrical delay self.add_parameter('electrical_delay', label='Electrical Delay', get_cmd='CALC:CORR:EDEL:TIME?', get_parser=float, set_cmd='CALC:CORR:EDEL:TIME {:.6e}', unit='s', vals=Numbers(min_value=0, max_value=100000)) # Sweep Time self.add_parameter('sweep_time', label='Time', get_cmd='SENS:SWE:TIME?', set_cmd='SENS:SWE:TIME {}', get_parser=float, unit='s', vals=Numbers(0, 1e6)) # Trigger Mode self.add_parameter('continuous_mode', label='Continuous Mode', get_cmd=':INIT:CONT?', set_cmd=':INIT:CONT {}', vals=Enum('on', 'On', 'ON', 1, 'off', 'Off', 'OFF', 0)) # Trigger Source self.add_parameter(name='trigger_source', label='Trigger source', get_cmd=":TRIG:SEQ:SOUR?", set_cmd=':TRIG:SEQ:SOUR {}', get_parser=str, vals=Enum('bus', 'BUS', 'Bus', 'EXT', 'external', 'EXTERNAL', 'External', 'INT', 'internal', 'INTERNAL', 'Internal', 'MAN', 'manual', 'MANUAL', 'Manual')) # Traces self.add_parameter(name='num_traces', label='Number of Traces', get_cmd='CALC:PAR:COUN?', set_cmd='CALC:PAR:Coun {}', get_parser=int, vals=Numbers(min_value=1, max_value=4)) self.add_parameter('active_trace', label='Active Trace', set_cmd="CALC:PAR{}:SEL", vals=Numbers(min_value=1, max_value=4)) #Init the names of the traces on the VNA for n in range(self.num_traces()): self.write("CALC:PAR{}:TNAME:DATA TR{}".format(n + 1, n + 1)) # Initialize the trigger source to "BUS" self.trigger_source('BUS') # Initialize sweep time to auto self.sweep_time(0) # Note: Traces will be accessed through the traces property which # updates the channellist to include only active trace numbers self._traces = ChannelList(self, "PNATraces", PNATrace) self.add_submodule("traces", self._traces) # # Add shortcuts to first trace # trace1 = self.traces[0] # for param in trace1.parameters.values(): # self.parameters[param.name] = param # # And also add a link to run sweep # self.run_sweep = trace1.run_sweep # # Set this trace to be the default (it's possible to end up in a # # situation where no traces are selected, causing parameter snapshots # # to fail) # self.active_trace(trace1.trace_num) # Add markers to instrument self._markers = ChannelList(self, "ENAMarkers", ENAMarker) self.add_submodule("markers", self._markers) # Set auto_sweep parameter # If we want to return multiple traces per setpoint without sweeping # multiple times, we should set this to false self.add_parameter('auto_sweep', label='Auto Sweep', set_cmd=None, get_cmd=None, vals=Bool(), initial_value=True) self.connect_message() @property def traces(self) -> ChannelList: """ Update channel list with active traces and return the new list """ # Get a list of traces from the instrument and fill in the traces list parlist = self.get_trace_catalog() self._traces.clear() for trace_name in parlist[::2]: trace_num = self.select_trace_by_name(trace_name) pna_trace = PNATrace(self, "TR{}".format(trace_num), trace_name, trace_num) self._traces.append(pna_trace) # Return the list of traces on the instrument return self._traces @property def markers(self) -> ChannelList: """ Update channel list with markers and return the new list """ self._markers.clear() for marker_num in range(1, 5): marker_name = "marker{}".format(marker_num) ena_marker = ENAMarker(self, "marker{}".format(marker_num), marker_name, marker_num) self._markers.append(ena_marker) # Return the list of markers return self._markers def get_options(self) -> Sequence[str]: # Query the instrument for what options are installed return self.ask('*OPT?').strip('"').split(',') def get_trace_catalog(self): """ Get the trace catalog, that is a list of trace and sweep types from the PNA. The format of the returned trace is: trace_name,trace_type,trace_name,trace_type... """ trace_catalog = [] for n in range(self.num_traces()): trace_catalog.append( self.ask(":CALC:PAR{}:TNAMe:DATA?".format(n + 1))[1:-1]) trace_catalog.append(self.ask('CALC:PAR{}:DEF?'.format(n + 1))) return trace_catalog def select_trace_by_name(self, trace_name: str) -> int: """ Select a trace on the PNA by name. Returns: The trace number of the selected trace """ self.write(f"CALC:PAR:TNAME:SEL '{trace_name}'") trace_catalog = self.get_trace_catalog() # print(int(trace_catalog.index(trace_name)/2+1)) return int(trace_catalog.index(trace_name) / 2 + 1) def reset_averages(self): """ Reset averaging """ self.write("SENS:AVER:CLE") def averages_on(self): """ Turn on trace averaging """ self.averages_enabled(True) def averages_off(self): """ Turn off trace averaging """ self.averages_enabled(False) def _set_power_limits(self, min_power: Union[int, float], max_power: Union[int, float]) -> None: """ Set port power limits """ self.power.vals = Numbers(min_value=min_power, max_value=max_power) for port in self.ports: port._set_power_limits(min_power, max_power) def single_trigger(self): self.write('TRIG:SING') def set_immediate_mode(self): self.write('INIT:IMM')