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)
Exemple #2
0
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)
Exemple #3
0
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
    })
Exemple #4
0
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
Exemple #5
0
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)
Exemple #6
0
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"""
Exemple #7
0
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)
Exemple #8
0
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)
Exemple #9
0
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)
Exemple #10
0
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)