class VNAProp(ZNB_gen): def __init__(self, *args, **kwargs): super(VNAProp, self).__init__(*args, **kwargs) self.cb_value = None def get_root(self): return self def cb(self, *args, **kwargs): assert "get" in kwargs last_cb = self.cb_value if not kwargs["get"]: assert "value" in kwargs v = kwargs["value"] self.cb_value = v if isinstance(v, dict): return v # dict passed to Instrument.write() else: self.cb_value = None if isinstance(last_cb, dict): return last_cb _SWE = ZNB_gen.SENSe.SWEep str_prop = SCPIProperty(_SWE.TYPE, str) int_prop = SCPIProperty(_SWE.POINts, int) int_prop_minmax = SCPIPropertyMinMax(int_prop) float_prop = SCPIProperty(_SWE.TIME, float) float_minmax = SCPIPropertyMinMax(float_prop) znb_only = SCPIPropertyMapping(_SWE.DWELl.IPOint, str, {"ALL": True, "FIRSt": False}) map_prop = SCPIPropertyMapping(_SWE.GENeration, str, mapping={"ANALog": True, "STEPped": False}) cb_prop = SCPIProperty(_SWE.TYPE, str, callback=cb, get_root_node=get_root)
class Marker(ZNB_gen.CALCulate.MARKer): """ Represents a trace marker in the VNA. Property access will make the trace associated with the marker the active trace in the channel. """ def __init__(self, n, trace): """ :param n: Marker number :param trace: The trace which the marker belongs to :type trace: Trace """ super(Marker, self).__init__(parent=trace.channel.CALC) self.n = n self.trace = trace self._cmd_cnt = None # noinspection PyUnusedLocal def _prop_callback(self, *args, **kwargs): if not self._cmd_cnt or self._cmd_cnt != self.trace.channel.instrument.command_cnt: self.trace.select_trace() self._cmd_cnt = self.trace.channel.instrument.command_cnt + 1 _MKR = ZNB_gen.CALCulate.MARKer tracking = SCPIProperty( _MKR.SEARch.TRACking, bool, callback=_prop_callback) #: Marker tracking enabled is_enabled = SCPIProperty(_MKR.STATe, bool, callback=_prop_callback) """Enable/disable the marker""" #: Marker position x = SCPIProperty(_MKR.X, float, callback=_prop_callback) #: Marker value y = SCPIProperty(_MKR.Y, float, callback=_prop_callback)
class SweepSegment(ZNB_gen.SENSe.SEGMent): def __init__(self, n, channel): """ :param n: The sweep segment number :param Channel channel: """ super(SweepSegment, self).__init__(parent=channel.SENSe) self.channel = channel self.n = n def delete(self): self.DELete().w() _SEG = ZNB_gen.SENSe.SEGMent dwell_time = SCPIProperty(_SEG.SWEep.DWELl, float) is_enabled = SCPIProperty(_SEG.STATe, bool) freq_start = SCPIProperty(_SEG.FREQuency.STARt, float) freq_stop = SCPIProperty(_SEG.FREQuency.STOP, float) if_bandwidth = SCPIProperty(_SEG.BWIDth.RESolution, int) # if_gain = SCPIProperty(_SEG.POWer.GAINcontrol, str) # TODO: this behaves differently from the other per segment settings... if_selectivity = SCPIProperty(_SEG.BWIDth.RESolution.SELect, str) number_of_points = SCPIProperty(_SEG.SWEep.POINts, int) power_level = SCPIProperty(_SEG.POWer, float) sweep_time = SCPIProperty(_SEG.SWEep.TIME, float) # type: float analog_sweep_is_enabled = SCPIPropertyMapping(_SEG.SWEep.GENeration, str, { "ANALog": True, "STEPped": False })
class NRPxxSN(NRPxxSN_gen): def __init__(self, visa_res): super(NRPxxSN, self).__init__(visa_res) def init(self): super(NRPxxSN, self).init() self._visa_res.timeout = 6000 # Zeroing the sensor takes ca 5 seconds self.ABORt.w() idn = self.IDN.q() self.visa_logger.info("NRP sensor initialized: %s", idn) def query_system_info(self): """ Queries SYSTem:INFO? and returns the respons parsed in to a dict() """ # SYST:INFO? => "A:B\nC:D\n....\n" info_list = str(self.SYSTem.INFO.q()).strip('" \n').split("\n") return dict([x.split(":", 1) for x in info_list]) def init_immediate(self): """ Sends INITiate:IMMediate to the sensor to begin a measurement cycle. """ self.INITiate.IMMediate.w() # TODO: this should be in Instrument def cal_zero(self): """ Run the zero level adjustment routine. Disconnect power from the sensor before running. """ self.CALibration.ZERO.AUTO().w("ONCE") def fetch_data(self): # type: () -> [float] """ Get the data from the measurement buffer using FETCh? :return: A list of floats """ return self.FETCh.q().split_comma(convert=float) def fetch_numpy(self): """ Get the data from the measurement buffer using FETCh? :return: The data from the measurement buffer, stored in a numpy array. """ return self.FETCh.q().numpy_array() frequency = SCPIProperty(NRPxxSN_gen.SENSe.FREQuency, float) frequency_minmax = SCPIPropertyMinMax(frequency) init_cont = SCPIProperty(NRPxxSN_gen.INITiate.CONTinuous, bool) # FIXME: This node should be a SCPIBool
class ChannelVNAPort(ZNB_gen.SOURce.POWer): def __init__(self, channel, port_no): super(ChannelVNAPort, self).__init__(parent=channel.SOURce) self.channel = channel self.n = port_no _POW = ZNB_gen.SOURce.POWer cal_power_offset = SCPIProperty(_POW.CORRection.LEVel.OFFSet, float) """This offset only changes the displayed port power, the source level is not affected""" power_enabled = SCPIProperty(_POW.STATe, bool) """Turn the source power on or off""" power_gen = SCPIProperty(_POW.PERManent.STATe, bool) """If power_gen is set to True the port power is on for all partial measurements.""" power_slope = SCPIProperty(_POW.LEVel.IMMediate.SLOPe, float) """Set a slope for the port power in dB/GHz""" def get_source_power_offset(self): """ The method returs a 2-tuple. The first element is the power offset in dB, the second element is True if the offset is relative to the channel base power. If false the first element is the port power in dBm. """ (power, rel) = self.LEVel.IMMediate.OFFSet().q().split_comma() return float(power), rel == "CPAD" def set_source_power_offset(self, power, relative=True): """ Set the port power effset. If relative is true `power` is an offset to the channel base power. If `relative` is False then power is the port power in dBm. :param power: Port power offset in dB, or port power in dBm. :param bool relative: Determines whether `power` is relative to the channel base power, or an absolute power. """ if relative: x = 'CPAD' else: x = 'ONLY' self.LEVel.IMMediate.OFFSet().w(power, x)
class ChannelVNAPort(ZVA_gen.SOURce.POWer, znb.ChannelVNAPort): @property def src_attenuator(self): """ Sets/queries the source attenuator value. If the attenuator setting is in auto mode, the current value of the attenuator will be returned. SOURce:POWer:ATTenuation / SOURce:POWer:ATTenuation:AUTO:VALue? """ return int(self.ATTenuation.AUTO.VALue().q()) @src_attenuator.setter def src_attenuator(self, att): # TODO: check that the att parameter is within the range of the instrument self.ATTenuation().w(int(att)) src_attenuator_mode = SCPIProperty(ZVA_gen.SOURce.POWer.ATTenuation.MODE, str) """AUTO | MANual | LNOise"""
class Sweep(ZNB_gen.SENSe.SWEep): # Using an Enum for the variuos sweep types only causes complications, with no clear benefit LIN = "LIN" LOG = "LOG" POWER = "POW" CW = "CW" POINT = "POIN" SEGMENT = "SEGM" def __init__(self, channel): """ :param Channel channel: """ super(Sweep, self).__init__(parent=channel.SENSe) self.channel = channel self.segments = SweepSegments(self) def get_segment(self, n): # type: (int) -> SweepSegment return SweepSegment(n, self.channel) _SWE = ZNB_gen.SENSe.SWEep analog_sweep_is_enabled = SCPIPropertyMapping(_SWE.GENeration, str, { "ANALog": True, "STEPped": False }) count = SCPIProperty(_SWE.COUNt, int) dwell_time = SCPIProperty(_SWE.DWELl, float) dwell_on_each_partial_measurement = SCPIPropertyMapping( _SWE.DWELl.IPOint, str, { "ALL": True, "FIRSt": False }) points = SCPIProperty(_SWE.POINts, int) points_minmax = SCPIPropertyMinMax(points) time = SCPIProperty(_SWE.TIME, float) time_minmax = SCPIPropertyMinMax(time) type = SCPIProperty(_SWE.TYPE, str) use_auto_time = SCPIProperty(_SWE.TIME.AUTO, bool) step_size = SCPIProperty(_SWE.STEP, float) step_size_minmax = SCPIPropertyMinMax(step_size)
class Diagram(ZNB_gen.DISPlay.WINDow): def __init__(self, n, instrument): """ :param n: Number of the diagram area :param instrument: Reference to the Instrument :type instrument: ZNB """ super(Diagram, self).__init__(parent=instrument.DISPlay) self.instrument = instrument self.n = n def delete(self): # FIXME: make some kind of callback to update all remaining Diagram instances?? requires a weakref dict. """ Remove the diagram area. Note that this will re-number all remaining diagrams, so use with care. Renumbering causes the diagram name to be reset to the diagram number, this is arguably a FW bug. Also deletes all traces assigned to the diagram. :return: """ self.STATe().w("OFF") def select_diagram(self): """ Make the diagram the active diagram. :return: None """ self.is_maximized = self.is_maximized _WIN = ZNB_gen.DISPlay.WINDow is_maximized = SCPIProperty(_WIN.MAXimize, bool) """ Displays the diagram on top of the other diagrams, filling the whole screen. """ name = SCPIProperty(_WIN.NAME, str) """ The diagram name, shown in upper right corner. Returned with DISPlay:CATalog? """ title = SCPIProperty(_WIN.TITLe.DATA, str) """ The diagram title, shown on screen. """ title_is_visible = SCPIProperty(_WIN.TITLe.STATe, bool) """ Determines whether the diagram title is shown or not. """ def save_screenshot(self, filename): """ Take a screenshot containing only this diagram. :param filename: The filname under which the screenshot will be saved on the instrument. :return: a File object representing the captured screenshot :rtype: File """ return self.instrument.save_screenshot(filename=filename, diagram=self) def query_assigned_traces(self): """ Get the traces assigned to the diagram :return: A generator returning Traces """ l = self.TRACe.CATalog().q() for wnr, name in l.comma_list_pairs(): ch_no = self.instrument.CONFigure.TRACe.CHANnel.NAME.ID.q(name) ch = self.instrument.get_channel(ch_no) yield ch.get_trace(name=name)
class Trace(object): """ A class representing a trace on the VNA. Instances are obtained via Channel.create_trace() and Channel.get_trace() """ class MeasParam(object): class S(MeasParamBase): def __str__(self): return "S{:02d}{:02d}{!s}".format(self.dst_port, self.src_port, self.detector) class Wave(MeasParamBase): def __init__(self, receiver, dst_port, src_port=None, detector=""): super(Trace.MeasParam.Wave, self).__init__(dst_port, src_port, detector) if src_port is None: self.src_port = self.dst_port self.receiver = str(receiver).upper() def __str__(self): return "{!s}{:02d}D{:02d}{!s}".format(self.receiver, self.dst_port, self.src_port, self.detector) def __init__(self, name, channel): """ :param name: The trace name :param Channel channel: The channel the trace belongs to """ self._n = None self._name = str(name) self.check_if_name_is_valid(self._name, raise_err=True) self.channel = channel self._cmd_cnt = None def _calc_node(self): return self.channel.CALC def _corr_node(self): return self.channel.CORRection def _sweep_node(self): return self.channel.SENSe.SWEep def _disp_node(self): return self.channel.instrument.DISPlay # noinspection PyUnusedLocal def _make_active_cb(self, *args, **kwargs): if self._cmd_cnt != self.channel.instrument.command_cnt: self.select_trace() self._cmd_cnt = self.channel.instrument.command_cnt + 1 def autoscale(self): """ Activate the autoscaling function for the trace """ self.channel.instrument.DISPlay.WINDow.TRACe.Y.SCALe.AUTO().w( "ONCE", self.name, fmt="{:s}, {:q}") def copy_data_to_mem(self, target_trace_name): self.check_if_name_is_valid(target_trace_name, raise_err=True) self.channel.instrument.TRACe.COPY().w(target_trace_name, self.name) return self.__class__(target_trace_name, self.channel) def copy_math_to_mem(self, target_trace_name): self.check_if_name_is_valid(target_trace_name, raise_err=True) self.channel.instrument.TRACe.COPY.MATH().w(target_trace_name, self.name) return self.__class__(target_trace_name, self.channel) def copy(self, new_name): # type: ([str, unicode]) -> Trace """ Create a copy of the trace. The trace is not assigned to a diagram area. :param new_name: The name of the new trace :return: A new Trace instance """ self.check_if_name_is_valid(new_name, raise_err=True) meas = self.measurement return self.channel.create_trace(new_name, meas) def delete(self): """ Deletes the trace. CALCulate<Ch>:PARameter:DELete """ self.channel.CALC.PARameter.DELete().w(self.name) @property def measurement(self): """ A string describing the measurement associated with the trace. See CALCulate<Ch>:PARameter:SDEFine for all possible options. Use the Trace.MeasParam... helpers for easier use. Example: tr1.measurement = tr1.MeasParam.Wave("b", 1, src_port=1, detector="sam") """ return str(self.channel.CALC.PARameter.MEASure().q(self.name)) @measurement.setter def measurement(self, param): self.channel.CALC.PARameter.MEASure().w(self.name, str(param)) @property def name(self): """ The trace name, must be unique in the recall set. :rtype: str """ return self._name @name.setter def name(self, name): name = str(name) self.check_if_name_is_valid(name, raise_err=True) self.channel.instrument.CONFigure.TRACe.REName().w(self.name, name) self._name = name @staticmethod def check_if_name_is_valid(name, raise_err=False): # type: (str) -> bool """ The first character of a trace name can be either one of the upper case letters A to Z, one of the lower case letters a to z, an underscore _ or a square bracket [ or ]. For all other characters of a trace name, the numbers 0 to 9 can be used in addition. :param name: A string which will be checked to see if it is a valid trace name :param raise_err: Raise a ValueError if the name is invalid """ ret = re.match(r"^[A-Za-z\[\]_][A-Za-z\[\]_0-9]*$", name) is not None if not ret and raise_err: raise ValueError("Invalid trace name '%s'" % name) return ret @property def n(self): """ :return: CONFigure.TRACe.NAME.ID? """ if not self._n: # The trace number doesn't change with trace add/delete, so it's ok to cache it. self._n = int(self.channel.instrument.CONFigure.TRACe.NAME.ID().q( self.name)) return self._n def query_cal_state_label(self): """ Returns the system error correction state label for the trace as a string. """ self._make_active_cb() return str(self._corr_node().SSTate.q()) # TODO: argument checking? trace_format = SCPIProperty(ZNB_gen.CALCulate.FORMat, str, callback=_make_active_cb, get_root_node=_calc_node) # noinspection PyUnusedLocal def _add_trace_name_arg_cb(self, value=None, **kwargs): ret = {"name": self.name, "fmt": "{name:q}"} if value is not None: ret["value"] = value ret["fmt"] = "{value:s}, {name:q}" return ret _SCALE = ZNB_gen.DISPlay.WINDow.TRACe.Y.SCALe scale_per_div = SCPIProperty(_SCALE.PDIVision, float, callback=_add_trace_name_arg_cb, get_root_node=_disp_node) scale_top = SCPIProperty(_SCALE.TOP, float, callback=_add_trace_name_arg_cb, get_root_node=_disp_node) scale_bottom = SCPIProperty(_SCALE.BOTTom, float, callback=_add_trace_name_arg_cb, get_root_node=_disp_node) ref_level = SCPIProperty(_SCALE.RLEVel, float, callback=_add_trace_name_arg_cb, get_root_node=_disp_node) ref_pos = SCPIProperty(_SCALE.RPOSition, float, callback=_add_trace_name_arg_cb, get_root_node=_disp_node) """ The reference position of the trace on the screen specified in percent, where 0 is the bottom and 100 is the top of the screen. """ source_port = SCPIProperty( ZNB_gen.SENSe.SWEep.SRCPort, int, callback=_make_active_cb, get_root_node=_sweep_node) # Logical port number of the simulus port math_equation = SCPIProperty(ZNB_gen.CALCulate.MATH.EXPRession.SDEFine, str, callback=_make_active_cb, get_root_node=_calc_node) math_is_enabled = SCPIProperty(ZNB_gen.CALCulate.MATH.STATe, bool, callback=_make_active_cb, get_root_node=_calc_node) math_is_wave_quantity = SCPIProperty(ZNB_gen.CALCulate.MATH.WUNit.STATe, bool, callback=_make_active_cb, get_root_node=_calc_node) def is_active(self): return self.channel.active_trace.name == self.name def select_trace(self): """ Makes the trace the active trace in the channel. """ self.channel.CALC.PARameter.SELect().w(self.name) def get_marker(self, n): """ :param n: Marker number :rtype: Marker """ return Marker(n, self) def assign_diagram(self, diagram): """ Assigns the trace to a diagram. :param diagram: An existing Diagram area :type diagram: Diagram """ diagram.TRACe.EFEed().w(self.name)
class Channel(object): def __init__(self, n, instrument): """ :param n: Channel number :param instrument: A SCPINode instance, linked to the instrument :type instrument: ZNB """ self.n = n self.instrument = instrument self.CALC = instrument.CALCulate(n) # type: ZNB_gen.CALCulate self.CONFch = instrument.CONFigure.CHANnel(n) self.SENSe = instrument.SENSe(n) self.CORRection = instrument.SENSe(n).CORRection self.SOURce = instrument.SOURce(n) self.TRIGger = instrument.TRIGger(n) # type: ZNB_gen.TRIGger self.sweep = self.get_sweep() name = SCPIProperty(ZNB_gen.CONFigure.CHANnel.NAME, str, get_root_node=lambda self: self.CONFch) """ The channel name, CONFigure:CHANnel<Ch>:NAME """ def get_trace(self, name): # type: ([unicode, str]) -> Trace """ :param name: The name of the trace :return: A Trace object """ return Trace(name=name, channel=self) def get_sweep(self): return Sweep(self) def get_vna_port(self, port_no): return ChannelVNAPort(self, port_no) def create_trace(self, name, parameter, diagram=None): """ Create a new trace with a measurement parameter according to CALCulate<Ch>:PARameter:SDEFine :param name: The trace name :param parameter: A string defining the measured quantity :param diagram: An optional Diagram, which the trace will be assigned to :type diagram: Diagram :return: A reference to the new trace :rtype: Trace """ self.CALC.PARameter.SDEFine().w(name, parameter) trace = self.get_trace(name) if diagram is not None: trace.assign_diagram(diagram) return trace @property def active_trace(self): """ Query or set the active trace in the channel :rtype: Trace """ name = str(self.CALC.PARameter.SELect().q()) # n = self.instrument.CONFigure.TRACe.CHANnel.NAME.ID.q(name) return self.get_trace(name) @active_trace.setter def active_trace(self, trace): name = trace.name if hasattr(trace, "name") else str(trace) self.CALC.PARameter.SELect().w(name) power_level = SCPIProperty( ZNB_gen.SOURce.POWer.LEVel.IMMediate.AMPLitude, float, get_root_node=lambda self: self.SOURce) # type: float """ The channel power level, in dBm. """ _FRQ = ZNB_gen.SENSe.FREQuency freq_cw = SCPIProperty(_FRQ.CW, float, get_root_node=lambda x: x.SENSe) freq_cw_minmax = SCPIPropertyMinMax(freq_cw) freq_start = SCPIProperty(_FRQ.STARt, float, get_root_node=lambda x: x.SENSe) freq_start_minmax = SCPIPropertyMinMax(freq_start) freq_stop = SCPIProperty(_FRQ.STOP, float, get_root_node=lambda x: x.SENSe) freq_stop_minmax = SCPIPropertyMinMax(freq_stop) freq_center = SCPIProperty(_FRQ.CENTer, float, get_root_node=lambda x: x.SENSe) freq_center_minmax = SCPIPropertyMinMax(freq_center) freq_span = SCPIProperty(_FRQ.SPAN, float, get_root_node=lambda x: x.SENSe) freq_span_minmax = SCPIPropertyMinMax(freq_span) ifbw = SCPIProperty(ZNB_gen.SENSe.BANDwidth, int, get_root_node=lambda x: x.SENSe) ifbw_minmax = SCPIPropertyMinMax(ifbw) def cal_auto(self, vna_ports, cal_unit_ports=None, cal_type="FNPort", cal_unit_characterization=""): if cal_unit_ports: cmd_fmt = "{:s}, {:q}, {:d**}" self.CORRection.COLLect.AUTO.PORTs.TYPE().w( cal_type, cal_unit_characterization, zip(vna_ports, cal_unit_ports), fmt=cmd_fmt) else: cmd_fmt = "{:s}, {:q}, {:d*}" self.CORRection.COLLect.AUTO.TYPE().w(cal_type, cal_unit_characterization, vna_ports, fmt=cmd_fmt) def configure_freq_sweep(self, start_freq, stop_freq, points=None, ifbw=None, power=None, log_sweep=False): """ Configure the instrument for a frequency sweep. Parameters which are not provided are left as is. :param float start_freq: Start frequency :param float stop_freq: Stop frequency :param int points: Number of sweep points :param float ifbw: Measurement IF bandwidth :param float power: Channel power setting, in dBm :param bool log_sweep: Sets the sweep type to LOGarithmic if True, LINear if False (default). """ if not log_sweep: self.sweep.type = Sweep.LIN else: self.sweep.type = Sweep.LOG self.freq_start = start_freq self.freq_stop = stop_freq if points is not None: self.sweep.points = points if ifbw is not None: self.ifbw = ifbw if power is not None: self.power_level = power def configure_power_sweep(self, freq, start_power, stop_power, points=None, ifbw=None): """ Configure the channel for a power sweep. Unspecified parameters are not modified. :param float freq: The CW frequency for the channel :param float start_power: Start power level in dBm :param float stop_power: Stop power level in dBm :param int points: Number of sweep points :param ifbw: IF bandwidth """ self.sweep.type = Sweep.POWER self.freq_cw = freq self.SOURce.POWer(1).STARt().w( start_power ) # The port number suffix on POWer is ignored by the instrument self.SOURce.POWer(1).STOP().w(stop_power) if points: self.sweep.points = points if ifbw: self.ifbw = ifbw def init_sweep(self): # FIXME: rename to start_sweep() or similar?? """ INITiate:IMMediate This is valid in single sweep mode only. """ self.instrument.INITiate(self.n).IMMediate().w() def save_touchstone(self, filename, ports, fmt="LOGPhase", mode_impedance="CIMPedance"): """ Save the S-parameters for the selected ports to a Touchstone file on the instrument. MMEMory:STORe:TRACe:PORTs :param filename: Desired filename :type filename: str :param ports: List of integers designating the logical ports which shall be included in the file :param fmt: Data format of the Touchstone file, default is "LOGPhase". "COMPlex" and "LINPhase" are the alternatives. :param mode_impedance: "CIMPedance" (default) or "PIMPedance". Determines if port impedances are renormalized according to common target impedance (50 ohm) or the individual port impedances. :return: A File object representing the stored file :rtype: ZNB.File """ cmd_fmt = "{:d}, {:q}, {:s}, {:s}, {:d*}" self.instrument.MMEMory.STORe.TRACe.PORTs().w(self.n, filename, fmt, mode_impedance, ports, fmt=cmd_fmt) return self.instrument.filesystem.file(filename)