Z = IMPEDANCE = TYPE_Z = 'Z' Y = ADMITTANCE = TYPE_Y = 'Y' S = SCATTERING = TYPE_S = 'S' T = SCATTERING_TRANSFER = TYPE_T = 'T' ABCD = TRANSMISSION = TYPE_ABCD = 'ABCD' Z = IMPEDANCE = TYPE_Z = 'Z'
Y = ADMITTANCE = TYPE_Y = 'Y'
S = SCATTERING = TYPE_S = 'S'
T = SCATTERING_TRANSFER = TYPE_T = 'T'
H = HYBRID = TYPE_H = 'H'
G = INVERSE_HYBRID = TYPE_G = 'G'
ABCD = TRANSMISSION = TYPE_ABCD = 'ABCD'
        P = type(cname, cbases, cdict)
class NPortMatrixBase(np.ndarray):
class NPortBase(NPortMatrixBase):
    def __new__(cls, matrix, type, z0=None):
    def _z0test(self, type, z0):
    def __repr__(self):
    def __array_finalize__(self, obj):
    def __reduce__(self):
    def __setstate__(self,state):
    def ports(self):
    def parameters(self):
    def get_parameter(self, port1, port2):
    def convert_z0test(self, type, z0):
    def __metaclass__(cname, cbases, cdict):
        def make_op(name, func):
            def op(this, other):
    def __new__(cls, freqs, matrices, type, z0=None):
    def __array_finalize__(self, obj):
    def __getitem__(self, index):
    def __getslice__(self, start, end):
    def __reduce__(self):
    def __setstate__(self,state):
    def parameters(self):
    def get_parameter(self, port1, port2):
    def get_element(self, port1, port2):
    def at(self, freqs):
    def average(self, n): Z = IMPEDANCE = TYPE_Z = 'Z'
Y = ADMITTANCE = TYPE_Y = 'Y'
S = SCATTERING = TYPE_S = 'S'
T = SCATTERING_TRANSFER = TYPE_T = 'T'
H = HYBRID = TYPE_H = 'H'
G = INVERSE_HYBRID = TYPE_G = 'G'
ABCD = TRANSMISSION = TYPE_ABCD = 'ABCD'
        P = type(cname, cbases, cdict)
class NPortMatrixBase(np.ndarray):
class NPortBase(NPortMatrixBase):
    def __new__(cls, matrix, type, z0=None):
    def _z0test(self, type, z0):
    def __repr__(self):
    def __array_finalize__(self, obj):
    def __reduce__(self):
    def __setstate__(self,state):
    def ports(self):
    def parameters(self):
    def get_parameter(self, port1, port2):
    def convert_z0test(self, type, z0):
    def __metaclass__(cname, cbases, cdict):
        def make_op(name, func):
            def op(this, other):
    def __new__(cls, freqs, matrices, type, z0=None):
    def __array_finalize__(self, obj):
    def __getitem__(self, index):
    def __getslice__(self, start, end):
    def __reduce__(self):
    def __setstate__(self,state):
    def parameters(self):
    def get_parameter(self, port1, port2):
    def get_element(self, port1, port2):
    def at(self, freqs):
    def average(self, n): class NPortMatrixBase(np.ndarray):
    """Base class representing an n-port matrix (Z, Y, S, T, G, H or ABCD)
    
    :type matrix: *n* by *n* array
    :param type: matrix type
    :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`,
                :data:`H` or :data:`ABCD`
    :param z0: normalizing impedance (only :class:`S` and :class:`T`)
    :type z0: :class:`float`

    """
    # TODO: implement type checking for operations (as in NPortBase)
    def __new__(cls, matrix, type, z0=None):
        # http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
        obj = matrix.view(cls)
        if type not in (Z, Y, S, T, H, G, ABCD):
            raise ValueError("illegal matrix type specified")
        obj.type = type
        obj.z0 = obj._z0test(type, z0)
        return obj

    def _z0test(self, type, z0):
        """Verify whether a normalizing impedance may be specified for the given
        n-port parameter type
        
        :param type: n-port parameter type
        :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`,
                    :data:`H` or :data:`ABCD`
        :param z0: normalizing impedance
        :type z0: :class:`float`
        
        """
        if type in (S, T):
            if z0 is None:
                z0 = 50.0
        elif z0 is not None:
            raise ValueError("the specified n-port representation (%s) does "
                             "not require a reference impedance" % type)
        return z0

    def __repr__(self):
        array_repr = repr(self.view(np.ndarray))[6:-1].replace('\n     ', '\n')
        attrs = 'type=%s' % self.type
        if self.type in (S, T):
            attrs += ', z0=%g' % self.z0
        return '%s(%s)\n %s' % (self.__class__.__name__,
                                attrs,
                                array_repr)
    
    def __array_finalize__(self, obj):
        if obj is None: return
        self.type = getattr(obj, 'type', None)
        self.z0 = getattr(obj, 'z0', None)

    def __reduce__(self):
        object_state = list(np.ndarray.__reduce__(self))
        subclass_state = (self.type, self.z0, )
        object_state[2] = (object_state[2], subclass_state)
        return tuple(object_state)
    
    def __setstate__(self,state):
        nd_state, own_state = state
        np.ndarray.__setstate__(self, nd_state)
        self.type, self.z0 = own_state

    @property
    def ports(self):
        """The number of ports of this :class:`NPortMatrixBase`
        
        :rtype: :class:`int`
        
        """
        raise NotImplementedError

    @property
    def parameters(self):
        """Return an iterator over the parameters in row-major order
        
        :returns: iterator over parameter
        :rtype: iterator of :class:`complex`
        
        """
        return self.flat

    def get_parameter(self, port1, port2):
        """Return the parameter as specified by the indices `port1` and `port2`
        as an ndarray
        
        :param port1: first index
        :type port1: :class:`int`
        :param port2: second index
        :type port2: :class:`int`
        :returns: parameter at indices `port1` and `port2`
        :rtype: :class:`complex`
        
        """
        return self[port1 - 1, port2 - 1]

    def convert_z0test(self, type, z0):
        """Check supplied `z0` for conversion and set default value depending on
        the type
        
        :param type: target type for conversion
        :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`,
                    :data:`H` or :data:`ABCD`
        :param z0: target normalization impedance (only :data:`S` and :data:`T`)
        :type z0: :class:`float`
        :returns: `z0` or default value when `z0 == None`
        :rtype: :class:`float`
        
        """
        if type in (S, T):
            if z0 is None:
                if self.type in (S, T):
                    z0 = self.z0
                else:
                    z0 = 50.0
        elif z0 is not None:
            raise ValueError("the specified n-port representation (%s) does"
                             " not require a reference impedance" % type)
        return z0 class NPortBase(NPortMatrixBase):
    """Base class representing an n-port across a list of frequencies

    :param freqs: list of frequency samples
    :type freqs: :class:`list`
    :param matrices: list of matrix elements
    :type matrix: list of 2 by 2 arrays in which each element is an 
                  *n* by *n* array
    :param type: matrix type
    :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`, :data:`H`
                or :data:`ABCD`
    :param z0: normalizing impedance (only :data:`S` and :data:`T`)
    :type z0: :class:`float`

    """
    matrix_cls = NPortMatrixBase
    
    def __metaclass__(cname, cbases, cdict):
        # http://thread.gmane.org/gmane.comp.python.general/373788/focus=373795
        P = type(cname, cbases, cdict)
        def make_op(name, func):
            def op(this, other):
                if issubclass(type(other), NPortBase):
                    if other.type == this.type:
                        if other.z0 != this.z0:
                            raise ValueError("operands have different Z0")
                        result_type = this.type
                        result_z0 = this.z0
                    else:
                        raise ValueError("operands have different types")
                    min_freq = max(this.freqs[0], other.freqs[0])
                    max_freq = min(this.freqs[-1], other.freqs[-1])
                    result_freqs = list(set(this.freqs).union(set(other.freqs)))
                    result_freqs.sort()
                    result_freqs = \
                        np.array(result_freqs[result_freqs.index(min_freq):
                                              result_freqs.index(max_freq)+1])
                    this_matrices = this.at(result_freqs)
                    other_matrices = other.at(result_freqs)
                    result_matrices = func(this_matrices, other_matrices)
                else:
                    result_freqs = this.freqs
                    result_matrices = func(this, other)
                    result_type = this.type
                    result_z0 = this.z0
                subclass = type(this)
                return subclass(result_freqs, result_matrices, result_type,
                                result_z0)
            op.__name__ = name
            return op
        for name, func in np.ndarray.__dict__.items():
            if callable(func) and name in ['__add__', '__sub__', '__mul__',
                '__div__', '__radd__', '__rsub__', '__rmul__', '__rdiv__']:
                setattr(P, name, make_op(name, func))
        return P

    def __new__(cls, freqs, matrices, type, z0=None):
        if len(freqs) != len(matrices):
            raise ValueError("the list of frequencies and the list of "
                             "matrices should have equal lenghts")
        if matrices[0].shape[0] != matrices[0].shape[1]:
            raise ValueError("the matrices should be square")
        obj = super(NPortBase, cls).__new__(cls, matrices, type, z0)
        obj.freqs = np.asarray(freqs)
        return obj

    def __array_finalize__(self, obj):
        super(NPortBase, self).__array_finalize__(obj)
        self.freqs = getattr(obj, 'freqs', None)

    def __getitem__(self, index):
        if type(index) == int:
            return self.matrix_cls(np.asarray(self)[index], self.type, self.z0)
        else:
            return np.asarray(self).__getitem__(index)

    def __getslice__(self, start, end):
        return self.__class__(self.freqs[start:end],
                              np.asarray(self)[start:end], self.type, self.z0)

    def __reduce__(self):
        object_state = list(super(NPortBase, self).__reduce__())
        subclass_state = (self.freqs, )
        object_state[2] = (object_state[2], subclass_state)
        return tuple(object_state)
    
    def __setstate__(self,state):
        super_state, own_state = state
        super(NPortBase, self).__setstate__(super_state)
        self.freqs, = own_state

    @property
    def parameters(self):
        """Return an iterator over the parameters in row-major order
        
        :returns: iterator over parameters
        :rtype: iterator over :class:`ndarray`
        
        """
        for row in range(self.ports):
            for column in range(self.ports):
                yield np.asarray(self[:, row, column])

    def get_parameter(self, port1, port2):
        """Return the parameter as specified by the indices `port1` and `port2`
        as an ndarray
        
        :param port1: first index
        :type port1: :class:`int`
        :param port2: second index
        :type port2: :class:`int`
        :returns: parameter at indices `port1` and `port2`
        :rtype: :class:`ndarray`
        
        """
        return np.asarray(self[:, port1 - 1, port2 - 1])

    def get_element(self, port1, port2):
        """Return the submatrices made up of the element as specified by the
        indices `port1` and `port2`
        
        :param port1: first index
        :type port1: :class:`int`
        :param port2: second index
        :type port2: :class:`int`
        :returns: submatrices at indices `port1` and `port2`
        :rtype: :class:`ndarray`
        
        """
        subclass = type(self)
        return subclass(self.freqs,
            np.asarray(self[:, (port1 - 1, ), :][:, :, (port2 - 1, )]),
            self.type, self.z0)

    def at(self, freqs):
        """Return the linearly interpolated n-port data at the given
        * list of frequencies (`freqs` is iterable), or
        * at a single frequency (`freqs` is a value)
        
        :param freqs: frequency point or list of frequencies at which to return
                      the n-port matrices
        :type freqs: :class:`float` or iterable of :class:`float`s
        :rtype: `type(self)`
        
        """
        # check whether freqs is iterable
        try:
            it = iter(freqs)
            single = False
        except TypeError:
            single = True
            freqs = [freqs]
        subclass = type(self)
        func = interpolate.interp1d(self.freqs, self, axis=0)
        interpolated = func(freqs)
        interpolated_nport = subclass(freqs, interpolated, self.type, self.z0)
        if single:
            return interpolated_nport[0]
        else:
            return interpolated_nport
            
    def average(self, n):
        """Take a moving average over `n` frequency samples
        
        :param n: number of samples to average over
        :type n: :class:`int`
        :rtype: `type(self)`
        
        """
        averaged = np.zeros_like(np.asarray(self))
        for i in range(len(self)):
            for j in range(- int(np.floor(n/2)), int(np.ceil(n/2))):
                index = i + j
                if index < 0:
                    index = 0
                elif index >= len(self):
                    index = len(self) - 1
                averaged[i] += self[index] / n        
        # numpy convolve
        #~ ones = np.ones(n) / n
        #~ averaged = np.zeros_like(np.asarray(self))
        #~ for i in range(self.ports):
            #~ for j in range(self.ports):
                #~ averaged[:, i, j] = np.convolve(ones, self[:, i, j])
        # or scipy convolve
        #~ averaged = scipy.signal.convolve(np.ones((n, ) + self.shape[1:])/n, np.asarray(self), 'same')
        subclass = type(self)
        return subclass(self.freqs, averaged, self.type, self.z0) class NPortMatrix(NPortMatrixBase):
    """Class representing an n-port matrix (Z, Y or S; for 2-ports also T, G,
    H or ABCD)

    :param matrix: matrix elements
    :type matrix: *n* by *n* array
    :param type: matrix type
    :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`, :data:`H`
                or :data:`ABCD`
    :param z0: normalizing impedance (only :class:`S` and :class:`T`)
    :type z0: :class:`float`
    
    """
    def __new__(cls, matrix, type, z0=None):
        matrix = np.asarray(matrix, dtype=complex)
        obj = NPortMatrixBase.__new__(cls, matrix, type, z0)
        if len(obj.shape) != 2:
            raise ValueError("the matrix should be two-dimensional")
        if obj.shape[0] != obj.shape[1]:
            raise ValueError("the matrix should be square")
        if obj.shape[0] == 2:
            obj.__class__ = TwoPortMatrix
        return obj

    @property
    def ports(self):
        """The number of ports of this :class:`NPortMatrix`
        
        :rtype: :class:`int`
        
        """
        return self.shape[0]

    def power(self, n):
        """Return this :class:`NPortMatrix` raised to the `n`\ th power
        
        :rtype: :class:`NPortMatrix`
        
        """
        return np.linalg.matrix_power(self, n)

    def twonportmatrix(self, inports=None, outports=None):
        """Return the 2n-port matrix represented by this n-port matrix
        
        :param inports: the list of ports that make up the inputs of the 2n-port
        :type inports: class:`tuple` or :class:`list`
        :param outports: the list of ports that make up the outputs
        :type outports: :class:`tuple` or :class:`list`
        :rtype: :class:`TwoNPortMatrix`
        
        """
        if self.ports % 2 != 0:
            raise TypeError("this NPortMatrix' number of ports is not a "
                            "multiple of 2")
        n = int(self.ports / 2)
        if inports is None and outports is None:
            inports = range(n)
            outports = range(n, 2*n)
            matrix = self
        else:
            # check whether the given sets of ports are valid
            assert inports is not None and outports is not None
            assert len(inports) == n
            assert len(outports) == n
            allports = set(inports).union(set(outports))
            assert len(allports) == 2*n
            assert min(allports) == 1 and max(allports) == 2*n
            inports = [port - 1 for port in inports]
            outports = [port - 1 for port in outports]

            ports = inports + outports

            # shuffle rows and columns to obtain 2n-port format
            matrix = []
            for row in ports:
                matrix.append(self[row])
            matrix = np.asarray(matrix).T
            matrix2 = []
            for column in ports:
                matrix2.append(matrix[column])
            matrix = np.asarray(matrix2).T

        # TODO: rewrite using reshape()?
        matrix = np.asmatrix(matrix)
        x11 = matrix[0:n  , 0:n  ]
        x12 = matrix[0:n  , n:2*n]
        x21 = matrix[n:2*n, 0:n  ]
        x22 = matrix[n:2*n, n:2*n]
        matrix = np.array([[x11, x12], [x21, x22]])
        return TwoNPortMatrix(matrix, self.type, self.z0)

    def reverse(self):
        """Return an :class:`NPortMatrix` with the ports reversed. This simply
        flips the matrix horizontally and vertically.
        
        :rtype: :class:`NPortMatrix`
        
        """
        return self[::-1, ::-1]

    def renormalize(self, z0):
        """Renormalize the n-port parameters to `z0`
        
        :param z0: new normalizing impedance
        :type z0: :class:`float`
        :rtype: :class:`NPortMatrix`
        
        """
        # http://qucs.sourceforge.net/tech/node98.html
        # "Renormalization of S-parameters to different port impedances"
        if self.type not in (S, T):
            raise TypeError("Only S and T matrices can be renormalized")

        if z0 == self.z0:
            result = self
        elif self.type == S:
            idty = np.identity(len(self), dtype=complex)

            r = (z0 - self.z0) / (z0 + self.z0)
            result = self.__class__(np.dot(self - idty * r,
                                           np.linalg.inv(idty - r * self)),
                                    self.type, z0)
        elif self.type == T:
            result = self.convert(S, z0).convert(T)

        return result

    def convert(self, type, z0=None):
        """Convert to another n-port matrix representation
        
        :param type: new matrix type
        :type type: :class:`Z`, :class:`Y` or :class:`S`
        :param z0: normalizing impedance (only :data:`S` and :data:`T`)
        :type z0: :class:`float`
        :rtype: :class:`NPortMatrix`

        """
        # references:
        #  * http://qucs.sourceforge.net/tech/node98.html
        #           "Transformations of n-Port matrices"
        #  * Multiport S-Parameter Measurements of Linear Circuits With Open
        #           Ports by Reimann et al. (only S <-> Z)
        #  * Agilent AN 154 - S-Parameter Design (S <-> T)
        #  * MATLAB S-parameter toolbox (Z, Y, H, G, ABCD, T)
        #           http://www.mathworks.com/matlabcentral/fileexchange/6080
        z0 = self.convert_z0test(type, z0)
        idty = np.identity(len(self), dtype=complex)
        invert = np.linalg.inv

        if type in (ABCD, T):
            raise TypeError("Cannot convert an NPort to %s-parameter "
                            "representation. Convert to a TwoNPort first" %
                            type)
        elif type in (H, G):
            if self.ports != 2:
                raise TypeError("Can only convert 2x2 matrices to %s-parameter "
                                "representation" % type)
        elif type not in (Z, Y, S):
            raise TypeError("Unknown n-port parameter type")

        # TODO: check for singularities
        if self.type == SCATTERING:
            if type == SCATTERING:
                return self.renormalize(z0)
            elif type == IMPEDANCE:
                result = (2 * invert(idty - self) - idty) * self.z0
            elif type == ADMITTANCE:
                result = (2 * invert(idty + self) - idty) / self.z0
        elif self.type == IMPEDANCE:
            if type == SCATTERING:
                result = idty - 2 * invert(idty + self / z0)
            elif type == ADMITTANCE:
                result = invert(self)
            elif type == IMPEDANCE:
                result = self
        elif self.type == ADMITTANCE:
            if type == SCATTERING:
                result = 2 * invert(idty + self * z0) - idty
            elif type == IMPEDANCE:
                result = invert(self)
            elif type == ADMITTANCE:
                result = self

        return NPortMatrix(result, type, z0)

    def submatrix(self, ports):
        """Keep only the parameters corresponding to the given ports,
        discarding the others. For a Z-matrix and a Y-matrix this corresponds to
        terminating the discarded ports in an open or short circuit
        respectively.

        :param ports: list of ports to keep
        :type ports: iterable
        :rtype: :class:`NPortMatrix`
        
        """
        indices = [port - 1 for port in ports]
        submatrix = self[indices, :][:, indices]
        return NPortMatrix(submatrix, self.type, self.z0)

    def recombine(self, portsets):
        """Recombine ports, reducing the number of ports of this NPortMatrix.
        
        :param portsets: an :class:`int` specifies the number of a port that
                         needs to be kept as-is. If the :class:`int` is
                         negative, the port's polarity will be reversed. A
                         :class:`tuple` specifies a pair of ports that are to be
                         recombined into a single port. The second element of
                         this :class:`tuple` acts as the ground reference to the
                         first element.
        :type portsets: iterable of :class:`int`\s and :class:`tuple`\s
        :rtype: :class:`NPortMatrix` (type :data:`Z`)
        
        >>> recombine([(1,3), (2,4), 5, -6]
        
        will generate a four-port where:
        
        * port 1 is original port 1 referenced to port 3
        * port 2 is original port 2 referenced to port 4
        * port 3 is original port 5
        * port 4 is original port 6, but with reversed polarity

        """
        if self.type == IMPEDANCE:
            m = []
            for i, ports in enumerate(portsets):
                row = [0 for port in range(self.ports)]
                try:
                    if isinstance(ports, tuple):
                        assert len(ports) == 2
                        row[ports[0] - 1] = 1
                        row[ports[1] - 1] = -1
                    else:
                        assert isinstance(ports, int)
                        assert ports != 0
                        if ports > 0:
                            row[ports - 1] = 1
                        else:
                            row[-ports - 1] = -1
                except IndexError:
                    raise IndexError("specified port number is higher than "
                                     "number of ports")
                m.append(row)
            m = np.matrix(m, dtype=float)
            result = m * np.asarray(self) * m.T
            return self.__class__(result, self.type, self.z0)
        else:
            z_recombined = self.convert(IMPEDANCE).recombine(portsets)
            return z_recombined

    def shunt(self, portsets):
        """Connect ports together, reducing the number of ports of this
        :class:`NPortMatrix`.
        
        :param portsets: an :class:`int` specifies the number of a port that
                         needs to be kept as-is. A :class:`tuple` specifies a
                         set of ports that are to be connected together.
        :type portsets: iterable of :class:`int`\s and :class:`tuple`\s
        :rtype: :class:`NPortMatrix` (type :data:`Y`)

        >>> shunt([1, (2, 3), (4, 5, 6)]
        
        will generate a three-port where:
        
        * port 1 is original port 1
        * port 2 is original port 2 and 3 connected together
        * port 3 is original ports 4, 5 and 6 connected together

        """
        # TODO: should be possible with m * self * m.T transformation, no?
        if self.type == ADMITTANCE:
            # columns
            tmp = np.zeros((self.ports, len(portsets)), dtype=complex)
            for i, ports in enumerate(portsets):
                try:
                    if isinstance(ports, tuple):
                        ports = [port - 1 for port in ports]
                        tmp[:, i] = np.sum(self[:, ports], 1)
                    else:
                        assert isinstance(ports, int)
                        assert ports > 0
                        tmp[:, i] = self[:, ports - 1]
                except IndexError:
                    raise IndexError("specified port number is higher than "
                                     "number of ports")
            # rows
            result = np.zeros((len(portsets), len(portsets)), dtype=complex)
            for i, ports in enumerate(portsets):
                try:
                    if isinstance(ports, tuple):
                        ports = [port - 1 for port in ports]
                        result[i, :] = np.sum(tmp[ports, :], 0)
                    else:
                        assert isinstance(ports, int)
                        assert ports > 0
                        result[i, :] = tmp[ports - 1, :]
                except IndexError:
                    raise IndexError("specified port number is higher than "
                                     "number of ports")
            return self.__class__(result, self.type, self.z0)
        else:
            y_shunted = self.convert(ADMITTANCE).shunt(portsets)
            return y_shunted

    def parallel(self, other, portmap=None):
        """Connect `other` in parallel with this :class:`NPortMatrix`.

        :param portmap: specifies to which ports of `other` should be connected
                        to the ports of this :class:`NPortMatrix`. A zero in the
                        n-th position indicates that nothing should be placed in
                        parallel with the port n. A positive integer in position
                        m indicates the port number of `other` that should be
                        connected to port m.
        :type portmap: iterable of :class:`int`\s
        :rtype: :class:`NPortMatrix` (type :data:`Y`)
        
        """
        assert len(portmap) == self.ports
        if other.type != ADMITTANCE:
            other = other.convert(ADMITTANCE)
        if portmap is None:
            assert other.ports == self.ports
            portmap = range(1, self.ports + 1)
        if self.type == ADMITTANCE:
            t = np.zeros((self.ports, other.ports), dtype=float)
            for i, port in enumerate(portmap):
                if port != 0:
                    t[i, port - 1] = 1
            t = np.matrix(t, dtype=float)
            other_reshape = t * np.asarray(other) * t.T
            return self.__class__(self + other_reshape, self.type, self.z0)
        else:
            y_paralleled = self.convert(ADMITTANCE).parallel(other, portmap)
            return y_paralleled

    def is_passive(self):
        """Check whether this n-port matrix is passive
        
        :rtype: :class:`bool`
        
        """
        if self.type != SCATTERING:
            return self.convert(SCATTERING).ispassive()
        else:
            return np.max(np.sum(np.abs(np.asarray(self))**2, 1)) <= 1

    def is_reciprocal(self):
        """Check whether this n-port matrix is reciprocal
        
        :rtype: :class:`bool`
        
        """
        raise NotImplementedError

    def is_symmetrical(self):
        """Check whether this n-port matrix is symmetrical
        
        :rtype: :class:`bool`
        
        """
        raise NotImplementedError class NPort(NPortBase):
    """Class representing an n-port across a list of frequencies
    
    :param freqs: list of frequency samples
    :type freqs: :class:`list`
    :param matrix: list of matrix elements
    :type matrix: list of *n* by *n* arrays
    :param type: matrix type
    :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`, :data:`H`
                or :data:`ABCD`
    :param z0: normalizing impedance (only :class:`S` and :class:`T`)
    :type z0: :class:`float`

    """
    matrix_cls = NPortMatrix
    
    def __new__(cls, freqs, matrices, type, z0=None):
        matrices = np.asarray(matrices, dtype=complex)
        obj = NPortBase.__new__(cls, freqs, matrices, type, z0)
        if len(obj[0].shape) != 2:
            raise ValueError("the matrices should be two-dimensional")
        if obj[0].shape[0] == 2:
            obj.__class__ = TwoPort
        return obj

    @property
    def ports(self):
        """The number of ports of this :class:`NPort`
        
        :rtype: :class:`int`
        
        """
        return self[0].ports

    def power(self, n):
        """Return this :class:`NPort`\'s matrices raised to the `n`\ th power
        
        :rtype: :class:`NPort`
        
        """
        matrices = [np.linalg.matrix_power(m, n) for m in self]
        return self.__class__(self.freqs, matrices, self.type, self.z0)

    def add(self, freq, matrix):
        """Return an NPort with the specified frequency sample added.

        :param freq: frequency at which to insert `matrix`
        :type freq: :class:`float`
        :param matrix: matrix to insert at `freq`
        :type matrix: :class:`NPortMatrix` or a complex array
        :rtype: :class:`NPort`

        If matrix is an :class:`NPortMatrix`, its elements will be converted to
        this :class:`NPort`'s type and characteristic impedance. If `matrix` is
        a complex array, it is assumed the elements are in the format of this
        :class:`NPort`.

        """
        if type(matrix) == NPortMatrix:
            if matrix.type != self.type or matrix.z0 != self.z0:
                matrix = matrix.convert(self.type, self.z0)
        index = self.freqs.searchsorted(freq)
        freqs = np.insert(self.freqs, index, freq)
        matrices = np.insert(self.matrices, index, matrix, 0)
        return self.__class__(freqs, matrices, self.type, self.z0)

    def twonport(self, inports=None, outports=None):
        """Convert this :class:`NPort` to a :class:`TwoNPort` using `inports`
        as the input ports and `outports` as the output ports.
        
        :param inports: the list of ports that make up the inputs of the 2n-port
        :type inports: :class:`tuple` or :class:`list`
        :param outports: the list of ports that make up the outputs
        :type outports: :class:`tuple` or :class:`list`
        :rtype: :class:`TwoNPort`

        """
        twonportmatrices = [m.twonportmatrix(inports, outports) for m in self]
        return TwoNPort(self.freqs, twonportmatrices, self.type, self.z0)

    def renormalize(self, z0):
        """Renormalize the n-port parameters to `z0`

        :param z0: new normalizing impedance
        :type z0: :class:`float`
        :rtype: :class:`NPort`

        """
        if self.type not in (S, T):
            raise TypeError("Only S and T matrices can be renormalized")

        if z0 == self.z0:
            result = self
        else:
            renormalized = [matrix.renormalize(z0) for matrix in self]
            result = self.__class__(self.freqs, renormalized, self.type, z0)
        return result

    def reverse(self):
        """Return an :class:`NPort` with the ports reversed. This simply flips
        the matrices horizontally and vertically.
        
        :rtype: :class:`NPort`
        
        """
        reversed = self[:, ::-1, ::-1]
        return NPort(self.freqs, reversed, self.type, self.z0)

    def convert(self, type, z0=None):
        """Convert to another n-port matrix representation
        
        :param type: n-port representation type to convert to
        :type type: :data:`Z`, :data:`Y`, :data:`S`, :data:`T`, :data:`G`,
                    :data:`H` or :data:`ABCD`
        :param z0: normalizing impedance (only :data:`S` and :data:`T`)
        :type z0: :class:`float`

        """
        converted = [matrix.convert(type, z0) for matrix in self]
        return NPort(self.freqs, converted, type, z0)

    def submatrix(self, ports):
        """Keep only the parameters corresponding to the given ports, discarding
        the others. For a Z-matrix and a Y-matrix this corresponds to
        terminating the discarded ports in an open or short circuit
        respectively.
        
        :param ports: list of ports to keep
        :type ports: iterable
        
        """
        indices = [port - 1 for port in ports]
        submatrices = self[:, indices, :][:, :, indices]
        return NPort(self.freqs, submatrices, self.type, self.z0)

    def invert(self):
        """Return an :class:`NPort` described by the inverse of this
        :class:`NPort`'s matrices
        
        :rtype: :class:`NPort`

        """
        # TODO: determine type of output
        inverted = [np.linalg.inv(matrix) for matrix in self]
        return NPort(self.freqs, inverted, self.type, self.z0)

    def recombine(self, portsets):
        """Recombine ports, reducing the number of ports of this :class:`NPort`.
        
        :param portsets: an :class:`int` specifies the number of a port that
                         needs to be kept as-is. If the :class:`int` is
                         negative, the port's polarity will be reversed. A
                         :class:`tuple` specifies a pair of ports that are to be
                         recombined into a single port. The second element of
                         this :class:`tuple` acts as the ground reference to the
                         first element.
        :type portsets: iterable of :class:`int`\s and :class:`tuple`\s
        :rtype: :class:`NPort`

        >>> recombine([(1,3), (2,4), 5, -6]
        
        will generate a four-port where:
        
        * port 1 is original port 1 referenced to port 3
        * port 2 is original port 2 referenced to port 4
        * port 3 is original port 5
        * port 4 is original port 6, but with reversed polarity

        """
        recombined = [matrix.recombine(portsets) for matrix in self]
        return self.__class__(self.freqs, recombined,
                              recombined[0].type, recombined[0].z0)

    def shunt(self, portsets):
        """Connect ports together, reducing the number of ports of this
        :class:`NPort`.
        
        :param portsets: an :class:`int` specifies the number of a port that
                         needs to be kept as-is. A :class:`tuple` specifies a
                         set of ports that are to be connected together.
        :type portsets: iterable of :class:`int`\s and :class:`tuple`\s
        :rtype: :class:`NPort`

        >>> shunt([1, (2, 3), (4, 5, 6)]
        
        will generate a three-port where:
        
        * port 1 is original port 1
        * port 2 is original port 2 and 3 connected together
        * port 3 is original ports 4, 5 and 6 connected together

        """
        shunted = [matrix.shunt(portsets) for matrix in self]
        return self.__class__(self.freqs, shunted,
                              shunted[0].type, shunted[0].z0)

    def parallel(self, other, portmap=None):
        """Connect `other` in parallel with this :class:`NPort`.

        :param portmap: specifies to which ports of `other` should be connected
                        to the ports of this :class:`NPort`. A zero in the n-th
                        position indicates that nothing should be placed in
                        parallel with the port n. A positive integer in position
                        m indicates the port number of `other` that should be
                        connected to port m.
        :type portmap: iterable of :class:`int`\s
        :rtype: :class:`NPort`
        
        """
        other = other.at(self.freqs)
        paralleled = [matrix.parallel(other_matrix, portmap)
                      for matrix, other_matrix in zip(self, other)]
        return self.__class__(self.freqs, paralleled,
                              paralleled[0].type, paralleled[0].z0)

    def is_passive(self):
        """Check whether this :class:`NPort` is passive
        
        :rtype: :class:`bool`
        
        """
        for matrix in self:
            if not matrix.is_passive():
                return False
        return True

    def group_delay(self, port1, port2):
        """Return the group delay of the parameter as specified by the indices
        `port1` and `port2`
        
        :param port1: index of input port
        :type port1: :class:`int`
        :param port2: index of output port
        :type port2: :class:`int`
        :returns: group delay of parameter at indices `port1` and `port2`
        :rtype: :class:`ndarray`
        
        """
        if self.type not in (S, ):
            raise TypeError("Group delay only makes sense for S-parameters (?)")
        phase = np.unwrap(rad(self.get_parameter(port1, port2)))
        dphase = np.gradient(phase)
        dfreq = np.gradient(self.freqs)
        return - dphase / (2 * np.pi * dfreq)
import numpy as np
from .base import Z, Y, S, T, ABCD
from .base import IMPEDANCE, ADMITTANCE, SCATTERING, SCATTERING_TRANSFER
from .base import HYBRID, INVERSE_HYBRID, TRANSMISSION 
from .base import NPortMatrixBase, NPortBase
from .nport import NPortMatrix, NPort
                t11 = s21i
                t12 = - s21i * s22
                t21 = s11 * s21i
                t22 = s12 - s11 * s21i * s22
                t = self.__class__([[t11, t12], [t21, t22]], T, z0)
                result = t.renormalize(z0)
        elif self.type == T and type == S:
            t11 = np.asmatrix(self[0,0])
            t12 = np.asmatrix(self[0,1])
            t21 = np.asmatrix(self[1,0])
            t22 = np.asmatrix(self[1,1])
            t11i = np.linalg.inv(t11)
            s11 = t21 * t11i
            s12 = t22 - t21 * t11i * t12
            s21 = t11i
            s22 = - t11i * t12
            result = self.__class__([[s11, s12], [s21, s22]], S, self.z0)
            if z0 != self.z0:
                result = result.renormalize(z0)
        elif self.type == T and type == ABCD:
            t11 = np.asmatrix(self[0,0])
            t12 = np.asmatrix(self[0,1])
            t21 = np.asmatrix(self[1,0])
            t22 = np.asmatrix(self[1,1])
            a = t11 + t12 + t21 + t22
            b = (t11 - t12 + t21 - t22) * self.z0
            c = (t11 + t12 - t21 -t22) / self.z0
            d = t11 - t12 -t21 + t22
            result = 0.5 * self.__class__([[a, b], [c, d]], ABCD, z0)
        elif self.type == ABCD and type == T:
            a = np.asmatrix(self[0,0])
            b0 = np.asmatrix(self[0,1]) / z0
            c0 = np.asmatrix(self[1,0]) * z0
            d = np.asmatrix(self[1,1])
            t11 = a + b0 + c0 + d
            t12 = a - b0 + c0 - d
            t21 = a + b0 - c0 - d
            t22 = a - b0 - c0 + d
            result = 0.5 * self.__class__([[t11, t12], [t21, t22]], T, z0)
        elif self.type == ABCD and type == Z:
            a = np.asmatrix(self[0,0])
            b = np.asmatrix(self[0,1])
            c = np.asmatrix(self[1,0])
            d = np.asmatrix(self[1,1])
            ci = np.linalg.inv(c)
            z11 = a * ci
            z12 = a * ci * d - b
            z21 = ci
            z22 = ci * d
            result = self.__class__([[z11, z12], [z21, z22]], Z, z0)
        elif self.type == Z and type == ABCD:
            z11 = np.asmatrix(self[0,0])
            z12 = np.asmatrix(self[0,1])
            z21 = np.asmatrix(self[1,0])
            z22 = np.asmatrix(self[1,1])
            z21i = np.linalg.inv(z21)
            a = z11 * z21i
            b = z11 * z21i * z22 - z12
            c = z21i
            d = z21i * z22
            result = self.__class__([[a, b], [c, d]], ABCD, z0)
        elif self.type == ABCD and type == Y:
            a = np.asmatrix(self[0,0])
            b = np.asmatrix(self[0,1])
            c = np.asmatrix(self[1,0])
            d = np.asmatrix(self[1,1])
            bi = np.linalg.inv(b)
            y11 = d * bi
            y12 = c - d * bi * a
            y21 = - bi
            y22 = bi * a
            result = self.__class__([[y11, y12], [y21, y22]], Y, z0)
        elif self.type == Y and type == ABCD:
            y11 = np.asmatrix(self[0,0])
            y12 = np.asmatrix(self[0,1])
            y21 = np.asmatrix(self[1,0])
            y22 = np.asmatrix(self[1,1])
            y21i = np.linalg.inv(y21)
            a = - y21i * y22
            b = - y21i
            c = y12 - y11 * y21i * y22
            d = - y11 * y21i
            result = self.__class__([[a, b], [c, d]], ABCD, z0)
        elif self.type == Z and type == T:
            z11 = np.asmatrix(self[0,0]) / z0
            z12 = np.asmatrix(self[0,1]) / z0
            z21 = np.asmatrix(self[1,0]) / z0
            z22 = np.asmatrix(self[1,1]) / z0
            z21i = np.linalg.inv(z21)
            i_plus_z11__times_z21i = (idty + z11) * z21i
            z11_minus_i__times_z21i = (z11 - idty) * z21i
            i_plus_z22 = (idty + z22)
            i_minus_z22 = (idty - z22)
            t11 = i_plus_z11__times_z21i * i_plus_z22 - z12
            t12 = i_plus_z11__times_z21i * i_minus_z22 + z12
            t21 = z11_minus_i__times_z21i * i_plus_z22 - z12
            t22 = z11_minus_i__times_z21i * i_minus_z22 + z12
            result = 0.5 * self.__class__([[t11, t12], [t21, t22]], T, z0)
        elif self.type == T and type == Z:
            # via scattering parameters
            s = self.convert(S).nportmatrix()
            z = s.convert(Z)
            return z.twonportmatrix()            
        elif self.type == Y and type == T:
            y11 = np.asmatrix(self[0,0]) * z0
            y12 = np.asmatrix(self[0,1]) * z0
            y21 = np.asmatrix(self[1,0]) * z0
            y22 = np.asmatrix(self[1,1]) * z0
            y21i = np.linalg.inv(y21)
            i_plus_y11__times_y21i = (idty + y11) * y21i
            i_minus_y11__times_y21i = (idty - y11) * y21i
            i_plus_y22 = (idty + y22)
            i_minus_y22 = (idty - y22)
            t11 = - i_plus_y11__times_y21i * i_plus_y22 + y12
            t12 = i_plus_y11__times_y21i * i_minus_y22 + y12
            t21 = - i_minus_y11__times_y21i * i_plus_y22 - y12
            t22 = i_minus_y11__times_y21i * i_minus_y22 - y12
            result = 0.5 * self.__class__([[t11, t12], [t21, t22]], T, z0)
        elif self.type == T and type == Y:
            # via scattering parameters

            s = self.convert(S).nportmatrix()