def test_gbt_with_sio_tf_and_zpk(self): """Test method='gbt' with alpha=0.25 for tf and zpk cases.""" # State space coefficients for the continuous SIO system. A = -1.0 B = 1.0 C = 1.0 D = 0.5 # The continuous transfer function coefficients. cnum, cden = ss2tf(A, B, C, D) # Continuous zpk representation cz, cp, ck = ss2zpk(A, B, C, D) h = 1.0 alpha = 0.25 # Explicit formulas, in the scalar case. Ad = (1 + (1 - alpha) * h * A) / (1 - alpha * h * A) Bd = h * B / (1 - alpha * h * A) Cd = C / (1 - alpha * h * A) Dd = D + alpha * C * Bd # Convert the explicit solution to tf dnum, dden = ss2tf(Ad, Bd, Cd, Dd) # Compute the discrete tf using cont2discrete. c2dnum, c2dden, dt = c2d((cnum, cden), h, method='gbt', alpha=alpha) assert_allclose(dnum, c2dnum) assert_allclose(dden, c2dden) # Convert explicit solution to zpk. dz, dp, dk = ss2zpk(Ad, Bd, Cd, Dd) # Compute the discrete zpk using cont2discrete. c2dz, c2dp, c2dk, dt = c2d((cz, cp, ck), h, method='gbt', alpha=alpha) assert_allclose(dz, c2dz) assert_allclose(dp, c2dp) assert_allclose(dk, c2dk)
def test_gbt_with_sio_tf_and_zpk(self): """Test method='gbt' with alpha=0.25 for tf and zpk cases.""" # State space coefficients for the continuous SIO system. A = -1.0 B = 1.0 C = 1.0 D = 0.5 # The continuous transfer function coefficients. cnum, cden = ss2tf(A, B, C, D) # Continuous zpk representation cz, cp, ck = ss2zpk(A, B, C, D) h = 1.0 alpha = 0.25 # Explicit formulas, in the scalar case. Ad = (1 + (1 - alpha) * h * A) / (1 - alpha * h * A) Bd = h * B / (1 - alpha * h * A) Cd = C / (1 - alpha * h * A) Dd = D + alpha * C * Bd # Convert the explicit solution to tf dnum, dden = ss2tf(Ad, Bd, Cd, Dd) # Compute the discrete tf using cont2discrete. c2dnum, c2dden, dt = d2c((cnum, cden), h, method='gbt', alpha=alpha) assert_allclose(dnum, c2dnum) assert_allclose(dden, c2dden) # Convert explicit solution to zpk. dz, dp, dk = ss2zpk(Ad, Bd, Cd, Dd) # Compute the discrete zpk using cont2discrete. c2dz, c2dp, c2dk, dt = d2c((cz, cp, ck), h, method='gbt', alpha=alpha) assert_allclose(dz, c2dz) assert_allclose(dp, c2dp) assert_allclose(dk, c2dk)
def pole_zero(sys, xlim=None, ylim=None, figax=None, rcParams=None): if len(sys) == 2: z, p, k = signal.tf2zpk(*sys) elif len(sys) == 3: z, p, k = sys elif len(sys) == 4: z, p, k = signal.ss2zpk(*sys) else: ValueError("""\ sys must have 2 (transfer function), 3 (zeros, poles, gain), or 4 (state space) elements. sys is: {}""".format(sys)) return _pole_zero(z, p, k, xlim=xlim, ylim=ylim, figax=figax, rcParams=rcParams)
def _get_zpk(arg, input=0): """Utility method to convert the input arg to a z, p, k representation. **Parameters:** arg, which may be: * ZPK tuple, * num, den tuple, * A, B, C, D tuple, * a scipy LTI object, * a sequence of the tuples of any of the above types. input : scalar In case the system has multiple inputs, which input is to be considered. Input `0` means first input, and so on. **Returns:** The sequence of ndarrays z, p and a scalar k **Raises:** TypeError, ValueError .. warn: support for MISO transfer functions is experimental. """ z, p, k = None, None, None if isinstance(arg, np.ndarray): # ABCD matrix A, B, C, D = partitionABCD(arg) z, p, k = ss2zpk(A, B, C, D, input=input) elif isinstance(arg, lti): z, p, k = arg.zeros, arg.poles, arg.gain elif _is_zpk(arg): z, p, k = np.atleast_1d(arg[0]), np.atleast_1d(arg[1]), arg[2] elif _is_num_den(arg): sys = lti(*arg).to_zpk() z, p, k = sys.zeros, sys.poles, sys.gain elif _is_A_B_C_D(arg): z, p, k = ss2zpk(*arg, input=input) elif isinstance(arg, collections.Iterable): ri = 0 for i in arg: # Note we do not check if the user has assembled a list with # mismatched lti representations. if hasattr(i, 'B'): iis = i.B.shape[1] if input < ri + iis: z, p, k = ss2zpk(i.A, i.B, i.C, i.D, input=input - ri) break else: ri += iis continue elif _is_A_B_C_D(arg): iis = arg[1].shape[1] if input < ri + iis: z, p, k = ss2zpk(*arg, input=input - ri) break else: ri += iis continue else: if ri == input: sys = lti(*i) z, p, k = sys.zeros, sys.poles, sys.gain break else: ri += 1 continue ri += 1 if (z, p, k) == (None, None, None): raise ValueError("The LTI representation does not have enough" + "inputs: max %d, looking for input %d" % (ri - 1, input)) else: raise TypeError("Unknown LTI representation: %s" % arg) return z, p, k
def filter(self, *filt): """Apply the given filter to this `TimeSeries`. All recognised filter arguments are converted either into cascading second-order sections (if scipy >= 0.16 is installed), or into the ``(numerator, denominator)`` representation before being applied to this `TimeSeries`. .. note:: All filters are presumed to be digital (Z-domain), if you have an analog ZPK (in Hertz or in rad/s) you should be using `TimeSeries.zpk` instead. .. note:: When using `scipy` < 0.16 some higher-order filters may be unstable. With `scipy` >= 0.16 higher-order filters are decomposed into second-order-sections, and so are much more stable. Parameters ---------- *filt one of: - :class:`scipy.signal.lti` - `MxN` `numpy.ndarray` of second-order-sections (`scipy` >= 0.16 only) - ``(numerator, denominator)`` polynomials - ``(zeros, poles, gain)`` - ``(A, B, C, D)`` 'state-space' representation Returns ------- result : `TimeSeries` the filtered version of the input `TimeSeries` See also -------- TimeSeries.zpk for instructions on how to filter using a ZPK with frequencies in Hertz scipy.signal.sosfilter for details on the second-order section filtering method (`scipy` >= 0.16 only) scipy.signal.lfilter for details on the filtering method Raises ------ ValueError If ``filt`` arguments cannot be interpreted properly """ sos = None # single argument given if len(filt) == 1: filt = filt[0] # detect LTI if isinstance(filt, signal.lti): filt = filt a = filt.den b = filt.num # detect SOS elif isinstance(filt, numpy.ndarray) and filt.ndim == 2: sos = filt # detect taps else: b = filt a = [1] # detect TF elif len(filt) == 2: b, a = filt elif len(filt) == 3: try: sos = signal.zpk2sos(*filt) except AttributeError: b, a = signal.zpk2tf(*filt) elif len(filt) == 4: try: zpk = signal.ss2zpk(*filt) sos = signal.zpk2sos(zpk) except AttributeError: b, a = signal.ss2tf(*filt) else: raise ValueError("Cannot interpret filter arguments. Please " "give either a signal.lti object, or a " "tuple in zpk or ba format. See " "scipy.signal docs for details.") if sos is not None: new = signal.sosfilt(sos, self, axis=0).view(self.__class__) else: new = signal.lfilter(b, a, self, axis=0).view(self.__class__) new.__dict__ = self.copy_metadata() return new
def _get_zpk(arg, input=0): """Utility method to convert the input arg to a z, p, k representation. **Parameters:** arg, which may be: * ZPK tuple, * num, den tuple, * A, B, C, D tuple, * a scipy LTI object, * a sequence of the tuples of any of the above types. input : scalar In case the system has multiple inputs, which input is to be considered. Input `0` means first input, and so on. **Returns:** The sequence of ndarrays z, p and a scalar k **Raises:** TypeError, ValueError .. warn: support for MISO transfer functions is experimental. """ z, p, k = None, None, None if isinstance(arg, np.ndarray): # ABCD matrix A, B, C, D = partitionABCD(arg) z, p, k = ss2zpk(A, B, C, D, input=input) elif hasattr(arg, '__class__') and arg.__class__.__name__ == 'lti': z, p, k = arg.zeros, arg.poles, arg.gain elif _is_zpk(arg): z, p, k = np.atleast_1d(arg[0]), np.atleast_1d(arg[1]), arg[2] elif _is_num_den(arg): sys = lti(*arg) z, p, k = sys.zeros, sys.poles, sys.gain elif _is_A_B_C_D(arg): z, p, k = ss2zpk(*arg, input=input) elif isinstance(arg, collections.Iterable): ri = 0 for i in arg: # Note we do not check if the user has assembled a list with # mismatched lti representations. if hasattr(i, 'B'): iis = i.B.shape[1] if input < ri + iis: z, p, k = ss2zpk(i.A, i.B, i.C, i.D, input=input - ri) break else: ri += iis continue elif _is_A_B_C_D(arg): iis = arg[1].shape[1] if input < ri + iis: z, p, k = ss2zpk(*arg, input=input - ri) break else: ri += iis continue else: if ri == input: sys = lti(*i) z, p, k = sys.zeros, sys.poles, sys.gain break else: ri += 1 continue ri += 1 if (z, p, k) == (None, None, None): raise ValueError("The LTI representation does not have enough" + "inputs: max %d, looking for input %d" % (ri - 1, input)) else: raise TypeError("Unknown LTI representation: %s" % arg) return z, p, k
def mapCtoD(sys_c, t=(0, 1), f0=0.0): """Map a MIMO continuous-time to an equiv. SIMO discrete-time system. The criterion for equivalence is that the sampled pulse response of the CT system must be identical to the impulse response of the DT system. i.e. If ``yc`` is the output of the CT system with an input ``vc`` taken from a set of DACs fed with a single DT input ``v``, then ``y``, the output of the equivalent DT system with input ``v`` satisfies: ``y(n) = yc(n-)`` for integer ``n``. The DACs are characterized by rectangular impulse responses with edge times specified in the t list. **Input:** sys_c : object the LTI description of the CT system, which can be: * the ABCD matrix, * a list-like containing the A, B, C, D matrices, * a list of zpk tuples (internally converted to SS representation), * a list of LTI objects. t : array_like The edge times of the DAC pulse used to make CT waveforms from DT inputs. Each row corresponds to one of the system inputs; [-1 -1] denotes a CT input. The default is [0 1], for all inputs except the first. f0 : float The (normalized) frequency at which the Gp filters' gains are to be set to unity. Default 0 (DC). **Output:** sys : tuple the LTI description for the DT equivalent, in A, B, C, D representation. Gp : list of lists the mixed CT/DT prefilters which form the samples fed to each state for the CT inputs. **Example:** Map the standard second order CT modulator shown below to its CT equivalent and verify that its NTF is :math:`(1-z^{-1})^2`. .. image:: ../doc/_static/mapCtoD.png :align: center :alt: mapCtoD block diagram It can be done as follows:: from __future__ import print_function import numpy as np from scipy.signal import lti from deltasigma import * LFc = lti([[0, 0], [1, 0]], [[1, -1], [0, -1.5]], [[0, 1]], [[0, 0]]) tdac = [0, 1] LF, Gp = mapCtoD(LFc, tdac) LF = lti(*LF) ABCD = np.vstack(( np.hstack((LF.A, LF.B)), np.hstack((LF.C, LF.D)) )) NTF, STF = calculateTF(ABCD) print("NTF:") # after rounding to a 1e-6 resolution print("Zeros:", np.real_if_close(np.round(NTF.zeros, 6))) print("Poles:", np.real_if_close(np.round(NTF.poles, 6))) Prints:: Zeros: [ 1. 1.] Poles: [ 0. 0.] Equivalent to:: (z -1)^2 NTF = ---------- z^2 .. seealso:: R. Schreier and B. Zhang, "Delta-sigma modulators employing \ continuous-time circuitry," IEEE Transactions on Circuits and Systems I, \ vol. 43, no. 4, pp. 324-332, April 1996. """ # You need to have A, B, C, D specification of the system Ac, Bc, Cc, Dc = _getABCD(sys_c) ni = Bc.shape[1] # Sanitize t if hasattr(t, "tolist"): t = t.tolist() if (type(t) == tuple or type(t) == list) and np.isscalar(t[0]): t = [t] # we got a simple list, like the default value if not (type(t) == tuple or type(t) == list) and not (type(t[0]) == tuple or type(t[0]) == list): raise ValueError("The t argument has an unrecognized shape") # back to business t = np.array(t) if t.shape == (1, 2) and ni > 1: t = np.vstack((np.array([[-1, -1]]), np.dot(np.ones((ni - 1, 1)), t))) if t.shape != (ni, 2): raise ValueError("The t argument has the wrong dimensions.") di = np.ones(ni).astype(bool) for i in range(ni): if t[i, 0] == -1 and t[i, 1] == -1: di[i] = False # c2d assumes t1=0, t2=1. # Also c2d often complains about poor scaling and can even produce # incorrect results. A, B, C, D, _ = cont2discrete((Ac, Bc, Cc, Dc), 1, method="zoh") Bc1 = Bc[:, ~di] # Examine the discrete-time inputs to see how big the # augmented matrices need to be. B1 = B[:, ~di] D1 = D[:, ~di] n = A.shape[0] t2 = np.ceil(t[di, 1]).astype(np.int_) esn = (t2 == t[di, 1]) and (D[0, di] != 0).T # extra states needed? npp = n + np.max(t2 - 1 + 1 * esn) # Augment A to npp x npp, B to np x 1, C to 1 x np. Ap = padb(padr(A, npp), npp) for i in range(n + 1, npp): Ap[i, i - 1] = 1 Bp = np.zeros((npp, 1)) if npp > n: Bp[n, 0] = 1 Cp = padr(C, npp) Dp = np.zeros((1, 1)) # Add in the contributions from each DAC for i in np.flatnonzero(di): t1 = t[i, 0] t2 = t[i, 1] B2 = B[:, i] D2 = D[:, i] if t1 == 0 and t2 == 1 and D2 == 0: # No fancy stuff necessary Bp = Bp + padb(B2, npp) else: n1 = np.floor(t1) n2 = np.ceil(t2) - n1 - 1 t1 = t1 - n1 t2 = t2 - n2 - n1 if t2 == 1 and D2 != 0: n2 = n2 + 1 extraStateNeeded = 1 else: extraStateNeeded = 0 nt = n + n1 + n2 if n2 > 0: if t2 == 1: Ap[:n, nt - n2 : nt] = Ap[:n, nt - n2 : nt] + np.tile(B2, (1, n2)) else: Ap[:n, nt - n2 : nt - 1] = Ap[:n, nt - n2 : nt - 1] + np.tile(B2, (1, n2 - 1)) Ap[:n, (nt - 1)] = Ap[:n, (nt - 1)] + _B2formula(Ac, 0, t2, B2) if n2 > 0: # pulse extends to the next period Btmp = _B2formula(Ac, t1, 1, B2) else: # pulse ends in this period Btmp = _B2formula(Ac, t1, t2, B2) if n1 > 0: Ap[:n, n + n1 - 1] = Ap[:n, n + n1 - 1] + Btmp else: Bp = Bp + padb(Btmp, npp) if n2 > 0: Cp = Cp + padr(np.hstack((np.zeros((D2.shape[0], n + n1)), D2 * np.ones((1, n2)))), npp) sys = (Ap, Bp, Cp, Dp) if np.any(~di): # Compute the prefilters and add in the CT feed-ins. # Gp = inv(sI - Ac)*(zI - A)/z*Bc1 n, m = Bc1.shape Gp = np.empty_like(np.zeros((n, m)), dtype=object) # !!Make this like stf: an array of zpk objects ztf = np.empty_like(Bc1, dtype=object) # Compute the z-domain portions of the filters ABc1 = np.dot(A, Bc1) for h in range(m): for i in range(n): if Bc1[i, h] == 0: ztf[i, h] = (np.array([]), np.array([0.0]), -ABc1[i, h]) # dt=1 else: ztf[i, h] = (np.atleast_1d(ABc1[i, h] / Bc1[i, h]), np.array([0.0]), Bc1[i, h]) # dt = 1 # Compute the s-domain portions of each of the filters stf = np.empty_like(np.zeros((n, n)), dtype=object) # stf[out, in] = zpk for oi in range(n): for ii in range(n): # Doesn't do pole-zero cancellation stf[oi, ii] = ss2zpk(Ac, np.eye(n), np.eye(n)[oi, :], np.zeros((1, n)), input=ii) # scipy as of v 0.13 has no support for LTI MIMO systems # only 'MISO', therefore you can't write: # stf = ss2zpk(Ac, eye(n), eye(n), np.zeros(n, n))) for h in range(m): for i in range(n): # k = 1 unneded, see below for j in range(n): # check the k values for a non-zero term if stf[i, j][2] != 0 and ztf[j, h][2] != 0: if Gp[i, h] is None: Gp[i, h] = {} Gp[i, h].update({"Hs": [list(stf[i, j])]}) Gp[i, h].update({"Hz": [list(ztf[j, h])]}) else: Gp[i, h].update({"Hs": Gp[i, h]["Hs"] + [list(stf[i, j])]}) Gp[i, h].update({"Hz": Gp[i, h]["Hz"] + [list(ztf[j, h])]}) # the MATLAB-like cell code for the above statements would have # been: # Gp[i, h](k).Hs = stf[i, j] # Gp[i, h](k).Hz = ztf[j, h] # k = k + 1 if f0 != 0: # Need to correct the gain terms calculated by c2d # B1 = gains of Gp @f0; for h in range(m): for i in range(n): B1ih = np.real_if_close(evalMixedTF(Gp[i, h], f0)) # abs() used because ss() whines if B has complex entries... # This is clearly incorrect. # I've fudged the complex stuff by including a sign.... B1[i, h] = np.abs(B1ih) * np.sign(np.real(B1ih)) if np.abs(B1[i, h]) < 1e-09: B1[i, h] = 1e-09 # This prevents NaN in "line 174" below # Adjust the gains of the pre-filters for h in range(m): for i in range(n): for j in range(max(len(Gp[i, h]["Hs"]), len(Gp[i, h]["Hz"]))): # The next is "line 174" Gp[i, h]["Hs"][j][2] = Gp[i, h]["Hs"][j][2] / B1[i, h] sys = ( sys[0], # Ap np.hstack((padb(B1, npp), sys[1])), # new B sys[2], # Cp np.hstack((D1, sys[3])), ) # new D return sys, Gp
def mapCtoD(sys_c, t=(0, 1), f0=0.): """Map a MIMO continuous-time to an equiv. SIMO discrete-time system. The criterion for equivalence is that the sampled pulse response of the CT system must be identical to the impulse response of the DT system. i.e. If ``yc`` is the output of the CT system with an input ``vc`` taken from a set of DACs fed with a single DT input ``v``, then ``y``, the output of the equivalent DT system with input ``v`` satisfies: ``y(n) = yc(n-)`` for integer ``n``. The DACs are characterized by rectangular impulse responses with edge times specified in the t list. **Input:** sys_c : object the LTI description of the CT system, which can be: * the ABCD matrix, * a list-like containing the A, B, C, D matrices, * a list of zpk tuples (internally converted to SS representation), * a list of LTI objects. t : array_like The edge times of the DAC pulse used to make CT waveforms from DT inputs. Each row corresponds to one of the system inputs; [-1 -1] denotes a CT input. The default is [0 1], for all inputs except the first. f0 : float The (normalized) frequency at which the Gp filters' gains are to be set to unity. Default 0 (DC). **Output:** sys : tuple the LTI description for the DT equivalent, in A, B, C, D representation. Gp : list of lists the mixed CT/DT prefilters which form the samples fed to each state for the CT inputs. **Example:** Map the standard second order CT modulator shown below to its CT equivalent and verify that its NTF is :math:`(1-z^{-1})^2`. .. image:: ../doc/_static/mapCtoD.png :align: center :alt: mapCtoD block diagram It can be done as follows:: from __future__ import print_function import numpy as np from scipy.signal import lti from deltasigma import * LFc = lti([[0, 0], [1, 0]], [[1, -1], [0, -1.5]], [[0, 1]], [[0, 0]]) tdac = [0, 1] LF, Gp = mapCtoD(LFc, tdac) LF = lti(*LF) ABCD = np.vstack(( np.hstack((LF.A, LF.B)), np.hstack((LF.C, LF.D)) )) NTF, STF = calculateTF(ABCD) print("NTF:") # after rounding to a 1e-6 resolution print("Zeros:", np.real_if_close(np.round(NTF.zeros, 6))) print("Poles:", np.real_if_close(np.round(NTF.poles, 6))) Prints:: Zeros: [ 1. 1.] Poles: [ 0. 0.] Equivalent to:: (z -1)^2 NTF = ---------- z^2 .. seealso:: R. Schreier and B. Zhang, "Delta-sigma modulators employing \ continuous-time circuitry," IEEE Transactions on Circuits and Systems I, \ vol. 43, no. 4, pp. 324-332, April 1996. """ # You need to have A, B, C, D specification of the system Ac, Bc, Cc, Dc = _getABCD(sys_c) ni = Bc.shape[1] # Sanitize t if hasattr(t, 'tolist'): t = t.tolist() if (type(t) == tuple or type(t) == list) and np.isscalar(t[0]): t = [t] # we got a simple list, like the default value if not (type(t) == tuple or type(t) == list) and \ not (type(t[0]) == tuple or type(t[0]) == list): raise ValueError("The t argument has an unrecognized shape") # back to business t = np.array(t) if t.shape == (1, 2) and ni > 1: t = np.vstack((np.array([[-1, -1]]), np.dot(np.ones((ni - 1, 1)), t))) if t.shape != (ni, 2): raise ValueError('The t argument has the wrong dimensions.') di = np.ones(ni).astype(bool) for i in range(ni): if t[i, 0] == -1 and t[i, 1] == -1: di[i] = False # c2d assumes t1=0, t2=1. # Also c2d often complains about poor scaling and can even produce # incorrect results. A, B, C, D, _ = cont2discrete((Ac, Bc, Cc, Dc), 1, method='zoh') Bc1 = Bc[:, ~di] # Examine the discrete-time inputs to see how big the # augmented matrices need to be. B1 = B[:, ~di] D1 = D[:, ~di] n = A.shape[0] t2 = np.ceil(t[di, 1]).astype(np.int_) esn = (t2 == t[di, 1]) and (D[0, di] != 0).T # extra states needed? npp = n + np.max(t2 - 1 + 1 * esn) # Augment A to npp x npp, B to np x 1, C to 1 x np. Ap = padb(padr(A, npp), npp) for i in range(n + 1, npp): Ap[i, i - 1] = 1 Bp = np.zeros((npp, 1)) if npp > n: Bp[n, 0] = 1 Cp = padr(C, npp) Dp = np.zeros((1, 1)) # Add in the contributions from each DAC for i in np.flatnonzero(di): t1 = t[i, 0] t2 = t[i, 1] B2 = B[:, i] D2 = D[:, i] if t1 == 0 and t2 == 1 and D2 == 0: # No fancy stuff necessary Bp = Bp + padb(B2, npp) else: n1 = np.floor(t1) n2 = np.ceil(t2) - n1 - 1 t1 = t1 - n1 t2 = t2 - n2 - n1 if t2 == 1 and D2 != 0: n2 = n2 + 1 extraStateNeeded = 1 else: extraStateNeeded = 0 nt = n + n1 + n2 if n2 > 0: if t2 == 1: Ap[:n, nt - n2:nt] = Ap[:n, nt - n2:nt] + np.tile(B2, (1, n2)) else: Ap[:n, nt - n2:nt - 1] = Ap[:n, nt - n2:nt - 1] + np.tile(B2, (1, n2 - 1)) Ap[:n, (nt - 1)] = Ap[:n, (nt - 1)] + _B2formula(Ac, 0, t2, B2) if n2 > 0: # pulse extends to the next period Btmp = _B2formula(Ac, t1, 1, B2) else: # pulse ends in this period Btmp = _B2formula(Ac, t1, t2, B2) if n1 > 0: Ap[:n, n + n1 - 1] = Ap[:n, n + n1 - 1] + Btmp else: Bp = Bp + padb(Btmp, npp) if n2 > 0: Cp = Cp + padr( np.hstack((np.zeros((D2.shape[0], n + n1)), D2 * np.ones( (1, n2)))), npp) sys = (Ap, Bp, Cp, Dp) if np.any(~di): # Compute the prefilters and add in the CT feed-ins. # Gp = inv(sI - Ac)*(zI - A)/z*Bc1 n, m = Bc1.shape Gp = np.empty_like(np.zeros((n, m)), dtype=object) # !!Make this like stf: an array of zpk objects ztf = np.empty_like(Bc1, dtype=object) # Compute the z-domain portions of the filters ABc1 = np.dot(A, Bc1) for h in range(m): for i in range(n): if Bc1[i, h] == 0: ztf[i, h] = (np.array([]), np.array([0.]), -ABc1[i, h] ) # dt=1 else: ztf[i, h] = (np.atleast_1d(ABc1[i, h] / Bc1[i, h]), np.array([0.]), Bc1[i, h]) # dt = 1 # Compute the s-domain portions of each of the filters stf = np.empty_like(np.zeros((n, n)), dtype=object) # stf[out, in] = zpk for oi in range(n): for ii in range(n): # Doesn't do pole-zero cancellation stf[oi, ii] = ss2zpk(Ac, np.eye(n), np.eye(n)[oi, :], np.zeros((1, n)), input=ii) # scipy as of v 0.13 has no support for LTI MIMO systems # only 'MISO', therefore you can't write: # stf = ss2zpk(Ac, eye(n), eye(n), np.zeros(n, n))) for h in range(m): for i in range(n): # k = 1 unneded, see below for j in range(n): # check the k values for a non-zero term if stf[i, j][2] != 0 and ztf[j, h][2] != 0: if Gp[i, h] is None: Gp[i, h] = {} Gp[i, h].update({'Hs': [list(stf[i, j])]}) Gp[i, h].update({'Hz': [list(ztf[j, h])]}) else: Gp[i, h].update( {'Hs': Gp[i, h]['Hs'] + [list(stf[i, j])]}) Gp[i, h].update( {'Hz': Gp[i, h]['Hz'] + [list(ztf[j, h])]}) # the MATLAB-like cell code for the above statements would have # been: #Gp[i, h](k).Hs = stf[i, j] #Gp[i, h](k).Hz = ztf[j, h] #k = k + 1 if f0 != 0: # Need to correct the gain terms calculated by c2d # B1 = gains of Gp @f0; for h in range(m): for i in range(n): B1ih = np.real_if_close(evalMixedTF(Gp[i, h], f0)) # abs() used because ss() whines if B has complex entries... # This is clearly incorrect. # I've fudged the complex stuff by including a sign.... B1[i, h] = np.abs(B1ih) * np.sign(np.real(B1ih)) if np.abs(B1[i, h]) < 1e-09: B1[i, h] = 1e-09 # This prevents NaN in "line 174" below # Adjust the gains of the pre-filters for h in range(m): for i in range(n): for j in range(max(len(Gp[i, h]['Hs']), len(Gp[i, h]['Hz']))): # The next is "line 174" Gp[i, h]['Hs'][j][2] = Gp[i, h]['Hs'][j][2] / B1[i, h] sys = ( sys[0], # Ap np.hstack((padb(B1, npp), sys[1])), # new B sys[2], # Cp np.hstack((D1, sys[3]))) # new D return sys, Gp
def realizeNTF_ct(ntf, form='FB', tdac=(0, 1), ordering=None, bp=None, ABCDc=None, method='LOOP'): """Realize an NTF with a continuous-time loop filter. **Parameters:** ntf : object A noise transfer function (NTF). form : str, optional A string specifying the topology of the loop filter. * 'FB': Feedback form, * 'FF': Feedforward form For the FB structure, the elements of ``Bc`` are calculated so that the sampled pulse response matches the L1 impulse response. For the FF structure, ``Cc`` is calculated. tdac : sequence, optional The timing for the feedback DAC(s). If ``tdac[0] >= 1``, direct feedback terms are added to the quantizer. Multiple timings (one or more per integrator) for the FB topology can be specified by making tdac a list of lists, e.g. ``tdac = [[1, 2], [1, 2], [[0.5, 1], [1, 1.5]], []]`` In this example, the first two integrators have DACs with ``[1, 2]`` timing, the third has a pair of DACs, one with ``[0.5, 1]`` timing and the other with ``[1, 1.5]`` timing, and there is no direct feedback DAC to the quantizer. ordering : sequence, optional A vector specifying which NTF zero-pair to use in each resonator Default is for the zero-pairs to be used in the order specified in the NTF. bp : sequence, optional A vector specifying which resonator sections are bandpass. The default (``zeros(...)``) is for all sections to be lowpass. ABCDc : ndarray, optional The loop filter structure, in state-space form. If this argument is omitted, ABCDc is constructed according to "form." method : str, optional The default fitting method is ``'LOOP'``, which means that the DT and CT loop responses will be matched. Alternatively, it is possible to set the method to ``'NTF'``, which will result in the NTF responses to be matched. See :ref:`discrete-time-to-continuous-time-mapping` for a more in-depth discussion. **Returns:** ABCDc : ndarray A state-space description of the CT loop filter tdac2 : ndarray A matrix with the DAC timings, including ones that were automatically added. **Example:** Realize the NTF :math:`(1 - z^{-1})^2` with a CT system (cf with the example at :func:`mapCtoD`).:: from deltasigma import * ntf = ([1, 1], [0, 0], 1) ABCDc, tdac2 = realizeNTF_ct(ntf, 'FB') Returns: ABCDc:: [[ 0. 0. 1. -1. ] [ 1. 0. 0. -1.49999999] [ 0. 1. 0. 0. ]] tdac2:: [[-1. -1.] [ 0. 1.]] """ ntf_z, ntf_p, _ = _get_zpk(ntf) ntf_z = carray(ntf_z) ntf_p = carray(ntf_p) order = max(ntf_p.shape) order2 = order // 2 odd = order - 2 * order2 # compensate for limited accuracy of zero calculation ntf_z[np.abs(ntf_z - 1) < eps**(1. / (1. + order))] = 1. method = method.upper() if method not in ('LOOP', 'NTF'): raise ValueError('Unimplemented matching method %s.' % method) # check if multiple timings mode if (type(tdac) == list or type(tdac) == tuple) and len(tdac) and \ (type(tdac[0]) == list or type(tdac[0]) == tuple): if len(tdac) != order + 1: msg = 'For multi-timing tdac, len(tdac) ' + \ ' must be order+1.' raise ValueError(msg) if form != 'FB': msg = "Currently only supporting form='FB' " + \ 'for multi-timing tdac' raise ValueError(msg) multi_timing = True else: # single timing tdac = carray(tdac) if np.prod(tdac.shape) != 2: msg = 'For single-timing tdac, len(tdac) must be 2.' raise ValueError(msg) tdac.reshape((2, )) multi_timing = False if ordering is None: ordering = np.arange(order2) if bp is None: bp = np.zeros((order2, )) if not multi_timing: # Need direct terms for every interval of memory in the DAC n_direct = int(np.ceil(tdac[1])) - 1 if tdac[0] > 0 and tdac[0] < 1 and tdac[1] > 1 and tdac[1] < 2: n_extra = n_direct - 1 # tdac pulse spans a sample point else: n_extra = n_direct tdac2 = np.vstack((np.array((-1, -1)), np.array(tdac).reshape( (1, 2)), 0.5 * np.dot(np.ones((n_extra, 1)), np.array([[-1, 1]])) + np.cumsum(np.ones( (n_extra, 2)), 0) + (n_direct - n_extra))) else: n_direct = 0 n_extra = 0 if ABCDc is None: ABCDc = np.zeros((order + 1, order + 2)) # Stuff the A portion if odd: ABCDc[0, 0] = np.real(np.log(ntf_z[0])) ABCDc[1, 0] = 1 dline = np.array([0, 1, 2]) for i in range(order2): n = bp[i] i1 = 2 * i + odd zi = 2 * ordering[i] + odd w = np.abs(np.angle(ntf_z[zi])) ABCDc[i1 + dline, i1] = np.array([0, 1, n]) ABCDc[i1 + dline, i1 + 1] = np.array([-w**2, 0, 1 - n]) ABCDc[0, order] = 1 # 2006.10.02 Changed to -1 to make FF STF have +ve gain at DC ABCDc[0, order + 1] = -1 Ac = ABCDc[:order, :order] if form == 'FB': Cc = ABCDc[order, :order].reshape((1, -1)) if not multi_timing: Bc = np.hstack((np.eye(order), np.zeros((order, 1)))) Dc = np.hstack((np.zeros((1, order)), np.array([[1]]))) tp = np.tile(np.array(tdac).reshape((1, 2)), (order + 1, 1)) else: #Assemble tdac2, Bc and Dc tdac2 = np.array([[-1, -1]]) Bc = None Dc = None Bci = np.hstack((np.eye(order), np.zeros((order, 1)))) Dci = np.hstack((np.zeros((1, order)), np.array([[1]]))) for i in range(len(tdac)): tdi = tdac[i] if (type(tdi) in (tuple, list)) and len(tdi) and \ (type(tdi[0]) in (list, tuple)): for j in range(len(tdi)): tdj = tdi[j] tdac2 = np.vstack((tdac2, np.array(tdj).reshape(1, -1))) if Bc is not None: Bc = np.hstack((Bc, Bci[:, i].reshape((-1, 1)))) else: Bc = Bci[:, i].reshape((-1, 1)) if Dc is not None: Dc = np.hstack((Dc, Dci[:, i].reshape((-1, 1)))) else: Dc = Dci[:, i].reshape((-1, 1)) elif len( tdi): # we got tdac[i] = [a, b] where a, b are scalars tdac2 = np.vstack((tdac2, np.array(tdi).reshape(1, -1))) if Bc is not None: Bc = np.hstack((Bc, Bci[:, i].reshape((-1, 1)))) else: Bc = Bci[:, i].reshape((-1, 1)) if Dc is not None: Dc = np.hstack((Dc, Dci[:, i].reshape((-1, 1)))) else: Dc = Dci[:, i].reshape((-1, 1)) tp = tdac2[1:, :] elif form == 'FF': Cc = np.vstack((np.eye(order), np.zeros((1, order)))) Bc = np.vstack((np.array([[-1]]), np.zeros((order - 1, 1)))) Dc = np.vstack((np.zeros((order, 1)), np.array([[1]]))) tp = tdac # 2008-03-24 fix from Ayman Shabra else: raise ValueError('Sorry, no code for form "%s".', form) n_imp = int(np.ceil(2 * order + np.max(tdac2[:, 1]) + 1)) if method == 'LOOP': # Sample the L1 impulse response y = impL1(ntf, n_imp) else: # Sample the NTF impulse response y = dimpulse((ntf_z, ntf_p, 1., 1.), t=np.arange(n_imp + 1))[1][0] y = np.atleast_1d(y.squeeze()) sys_c = [] for i in range(Bc.shape[1]): # number of inputs sys_tmp = [] for j in range(Cc.shape[0]): # number of outputs sys_tmp.append(ss2zpk(Ac, Bc, Cc[j, :], Dc[j, :], input=i)) sys_c.append(sys_tmp) yy = pulse(sys_c, tp, 1, n_imp, 1) yy = np.squeeze(yy) # Endow yy with n_extra extra impulses. # These will need to be implemented with n_extra extra DACs. # !! Note: if t1=int, matlab says pulse(sys) @t1 ~=0 # !! This code corrects this problem. if n_extra > 0: y_right = padb(np.vstack((np.zeros((1, n_direct)), np.eye(n_direct))), n_imp + 1) # Replace the last column in yy with an ordered set of impulses if (n_direct > n_extra): yy = np.hstack((yy, y_right[:, 1:])) else: yy = np.hstack((yy[:, :-1], y_right)) if method == 'NTF': # convolve CT loop response and NTF response yynew = None for i in range(yy.shape[1]): yytmp = np.convolve(yy[:, i], y)[:-n_imp] if yynew is None: yynew = yytmp.reshape((-1, 1)) else: yynew = np.hstack((yynew, yytmp.reshape((-1, 1)))) yy = yynew e1 = np.zeros(y.shape) e1[0] = 1. y = y - e1 # Solve for the coefficients x = linalg.lstsq(yy, y)[0] if linalg.norm(np.dot(yy, x) - y) > 0.0001: warn('Pulse response fit is poor.') if form == 'FB': if not multi_timing: Bc2 = np.hstack((x[:order].reshape( (-1, 1)), np.zeros((order, n_extra)))) if n_extra > 0: Dc2 = np.hstack((np.array([[0]]), x[order:].reshape((-1, 1)))) else: Dc2 = x[order:].reshape((-1, 1)) else: BcDc = np.vstack((Bc, Dc)) i = np.nonzero(BcDc) BcDc[i] = x Bc2 = BcDc[:-1, :] Dc2 = BcDc[-1, :] elif form == 'FF': Bc2 = np.hstack((Bc, np.zeros((order, n_extra)))) Cc = x[:order].reshape((1, -1)) if n_extra > 0: Dc2 = np.hstack((np.array([[0]]), x[order:].T)) else: Dc2 = x[order:].T Dc1 = np.zeros((1, 1)) Dc = np.hstack((Dc1, np.atleast_2d(Dc2))) Bc1 = np.vstack((np.ones((1, 1)), np.zeros((order - 1, 1)))) Bc = np.hstack((Bc1, Bc2)) # Scale Bc1 for unity STF magnitude at f0 fz = np.angle(ntf_z) / (2 * np.pi) f1 = fz[0] ibz = np.abs(fz - f1) <= np.abs(fz + f1) fz = fz[ibz] f0 = np.mean(fz) if np.min(np.abs(fz)) < 3 * np.min(np.abs(fz - f0)): f0 = 0 L0c = ss2zpk(Ac, Bc1, Cc, Dc1) G0 = evalTFP(L0c, ntf, f0) if f0 == 0: Bc[:, 0] = np.dot( Bc[:, 0], np.abs( np.dot(Bc[0, 1:], (tdac2[1:, 1] - tdac2[1:, 0])) / Bc[0, 0])) else: Bc[:, 0] = Bc[:, 0] / np.abs(G0) ABCDc = np.vstack((np.hstack((Ac, Bc)), np.hstack((Cc, Dc)))) #ABCDc = np.dot(ABCDc, np.abs(ABCDc) > eps**(1./2.)) ABCDc[np.nonzero(np.abs(ABCDc) < eps**(1. / 2))] = 0. return ABCDc, tdac2
def calculateTF(ABCD, k=1.): """Calculate the NTF and STF of a delta-sigma modulator. The calculation is performed for a given loop filter ABCD matrix, assuming a quantizer gain of ``k``. **Parameters:** ABCD : array_like, The ABCD matrix that describes the system. k : float or ndarray-like, optional The quantizer gains. If only one quantizer is present, it may be set to a float, corresponding to the quantizer gain. If multiple quantizers are present, a list should be used, with quantizer gains ordered according to the order in which the quantizer inputs appear in the ``C`` and ``D`` submatrices. If not specified, a default of one quantizer with gain ``1.`` is assumed. **Returns:** (NTF, STF) : a tuple of two LTI objects (or of two lists of LTI objects). If a version of the ``scipy`` library equal to 0.16.x or greater is in use, the objects will be ``ZeroPolesGain`` objects, a subclass of ``scipy.signal.lti``. If the system has multiple quantizers, multiple STFs and NTFs will be returned. In that case: * ``STF[i]`` is the STF from ``u`` to output number ``i``. * ``NTF[i, j]`` is the NTF from the quantization noise of the quantizer number ``j`` to output number ``i``. **Note:** Setting ``k`` to a list is unsupported in the MATLAB code (last checked Nov. 2014). **Example:** Realize a fifth-order modulator with the cascade-of-resonators structure, feedback form. Calculate the ABCD matrix of the loop filter and verify that the NTF and STF are correct. .. code-block:: python from deltasigma import * H = synthesizeNTF(5, 32, 1) a, g, b, c = realizeNTF(H) ABCD = stuffABCD(a,g,b,c) ntf, stf = calculateTF(ABCD) From which we get: ``H``:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) coefficients:: a: 0.0007, 0.0084, 0.055, 0.2443, 0.5579 g: 0.0028, 0.0079 b: 0.0007, 0.0084, 0.055, 0.2443, 0.5579, 1.0 c: 1.0, 1.0, 1.0, 1.0, 1.0 ABCD matrix:: [[ 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.75559806e-04 -6.75559806e-04] [ 1.00000000e+00 1.00000000e+00 -2.79396240e-03 0.00000000e+00 0.00000000e+00 8.37752565e-03 -8.37752565e-03] [ 1.00000000e+00 1.00000000e+00 9.97206038e-01 0.00000000e+00 0.00000000e+00 6.33294166e-02 -6.33294166e-02] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 -7.90937431e-03 2.44344030e-01 -2.44344030e-01] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 9.92090626e-01 8.02273699e-01 -8.02273699e-01] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00]] NTF:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) STF:: 1 """ nq = len(k) if type(k) in (tuple, list) else 1 A, B, C, D = partitionABCD(ABCD, m=nq + 1, r=nq) k = carray(k) diagk = np.atleast_2d(np.diag(k)) B1 = B[:, 0] B2 = B[:, 1:] B1 = B1.reshape((B1.shape[0], 1)) if len(B1.shape) == 1 else B1 B2 = B2.reshape((B2.shape[0], 1)) if len(B2.shape) == 1 else B2 # In a single-quantizer system, D2 should be all zeros, as any # non-zero term in D2 would imply we got a delay-free loop. # The original MATLAB code implies so. # The trouble arises when we adapt and extend the code to calculate # the transfer functions for multiple-quantizer systems. There the # off-diagonal terms of D2 may very well be non-zero, therefore # in the following we consider D2. # this means that if you supply an ABCD matrix, with one quantizer # and an (erroneously) zero-delay loop, the MATLAB toolbox will # disregard D2 and pretend it's zero and the loop is correctly # delayed, spitting out the corresponding TF. # Instead here we print out a warning and process the ABCD matrix # with the user-supplied, non-zero D2 matrix. # The resulting TFs will obviously be different. D1 = D[:, 0] D2 = D[:, 1:] D1 = D1.reshape((D1.shape[0], 1)) if len(D1.shape) == 1 else D1 D2 = D2.reshape((D2.shape[0], 1)) if len(D2.shape) == 1 else D2 # WARN DELAY FREE LOOPS if np.diag(D2).any(): warn("Delay free loop detected! D2 diag: %s", str(np.diag(D2))) # Find the noise transfer function by forming the closed-loop # system (sys_cl) in state-space form. Ct = np.linalg.inv(np.eye(nq) - D2 * diagk) Acl = A + np.dot(B2, np.dot(Ct, np.dot(diagk, C))) Bcl = np.hstack( (B1 + np.dot(B2, np.dot(Ct, np.dot(diagk, D1))), np.dot(B2, Ct))) Ccl = np.dot(Ct, np.dot(diagk, C)) Dcl = np.dot(Ct, np.hstack((np.dot(diagk, D1), np.eye(nq)))) tol = min(1e-3, max(1e-6, eps**(1 / ABCD.shape[0]))) ntfs = np.empty((nq, Dcl.shape[0]), dtype=np.object_) stfs = np.empty((Dcl.shape[0], ), dtype=np.object_) # sweep the outputs 'cause scipy is silly but we love it anyhow. for i in range(Dcl.shape[0]): # input #0 is the signal # inputs #1,... are quantization noise stf_z, stf_p, stf_k = ss2zpk(Acl, Bcl, Ccl[i, :], Dcl[i, :], input=0) stf = lti(stf_z, stf_p, stf_k) for j in range(nq): ntf_z, ntf_p, ntf_k = ss2zpk(Acl, Bcl, Ccl[i, :], Dcl[i, :], input=j + 1) ntf = lti(ntf_z, ntf_p, ntf_k) stf_min, ntf_min = minreal((stf, ntf), tol) ntfs[i, j] = ntf_min stfs[i] = stf_min # if we have one stf and one ntf, then just return those in a list if ntfs.shape == (1, 1): return [ntfs[0, 0], stfs[0]] return ntfs, stfs
def calculateTF(ABCD, k=1.): """Calculate the NTF and STF of a delta-sigma modulator. The calculation is performed for a given loop filter ABCD matrix, assuming a quantizer gain of ``k``. **Parameters:** ABCD : array_like, The ABCD matrix that describes the system. k : float, optional The quantizer gain. If not specified, a default value of 1 is used. **Returns:** (NTF, STF) : a tuple of two LTI objects. **Example:** Realize a fifth-order modulator with the cascade-of-resonators structure, feedback form. Calculate the ABCD matrix of the loop filter and verify that the NTF and STF are correct. .. code-block:: python from deltasigma import * H = synthesizeNTF(5, 32, 1) a, g, b, c = realizeNTF(H) ABCD = stuffABCD(a,g,b,c) ntf, stf = calculateTF(ABCD) From which we get: ``H``:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) coefficients:: a: 0.0007, 0.0084, 0.055, 0.2443, 0.5579 g: 0.0028, 0.0079 b: 0.0007, 0.0084, 0.055, 0.2443, 0.5579, 1.0 c: 1.0, 1.0, 1.0, 1.0, 1.0 ABCD matrix:: [[ 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.75559806e-04 -6.75559806e-04] [ 1.00000000e+00 1.00000000e+00 -2.79396240e-03 0.00000000e+00 0.00000000e+00 8.37752565e-03 -8.37752565e-03] [ 1.00000000e+00 1.00000000e+00 9.97206038e-01 0.00000000e+00 0.00000000e+00 6.33294166e-02 -6.33294166e-02] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 -7.90937431e-03 2.44344030e-01 -2.44344030e-01] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 9.92090626e-01 8.02273699e-01 -8.02273699e-01] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00]] NTF:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) STF:: 1 """ A, B, C, D = partitionABCD(ABCD) if B.shape[1] > 1: B1 = B[:, 0] B2 = B[:, 1] B1 = B1.reshape((B1.shape[0], 1)) if len(B1.shape) == 1 else B1 B2 = B2.reshape((B2.shape[0], 1)) if len(B2.shape) == 1 else B2 else: B1 = B B2 = B # Find the noise transfer function by forming the closed-loop # system (sys_cl) in state-space form. Acl = A + k * np.dot(B2, C) Bcl = np.hstack((B1 + k*B2*D[0, 0], B2)) Ccl = k*C Dcl = np.array((k*D[0, 0], 1.)) Dcl = Dcl.reshape((1, Dcl.shape[0])) if len(Dcl.shape) == 1 else Dcl tol = min(1e-3, max(1e-6, eps**(1/ABCD.shape[0]))) # input #0 is the signal # input #1 is the quantization noise stf_p, stf_z, stf_k = ss2zpk(Acl, Bcl, Ccl, Dcl, input=0) ntf_p, ntf_z, ntf_k = ss2zpk(Acl, Bcl, Ccl, Dcl, input=1) stf = lti(stf_p, stf_z, stf_k) ntf = lti(ntf_p, ntf_z, ntf_k) stf_min, ntf_min = minreal((stf, ntf), tol) return ntf_min, stf_min
_sys2ss = { _LSYS: lambda sys: _ss(sys.ss), _LFILT: lambda sys: _ss(tf2ss(sys.num, sys.den)), _NUM: lambda sys: _ss(tf2ss(sys, 1)), _TF: lambda sys: _ss(tf2ss(*sys)), _ZPK: lambda sys: _ss(zpk2ss(*sys)), _SS: lambda sys: _ss(sys), } _sys2zpk = { _LSYS: lambda sys: sys.zpk, _LFILT: lambda sys: tf2zpk(sys.num, sys.den), _NUM: lambda sys: tf2zpk(sys, 1), _TF: lambda sys: tf2zpk(*sys), _ZPK: lambda sys: sys, _SS: lambda sys: ss2zpk(*sys), } _sys2tf = { _LSYS: lambda sys: sys.tf, _LFILT: lambda sys: _tf(sys.num, sys.den), _NUM: lambda sys: _tf(sys, 1), _TF: lambda sys: _tf(*sys), _ZPK: lambda sys: _tf(*zpk2tf(*sys)), _SS: lambda sys: _tf(*_ss2tf(*sys)), } def sys2ss(sys): """Converts an LTI system in any form to state-space.""" return _sys2ss[_sys2form(sys)](sys)
def realizeNTF_ct(ntf, form='FB', tdac=(0, 1), ordering=None, bp=None, ABCDc=None): """Realize an NTF with a continuous-time loop filter. **Parameters:** ntf : object A noise transfer function (NTF). form : str, optional A string specifying the topology of the loop filter. * 'FB': Feedback form, * 'FF': Feedforward form For the FB structure, the elements of ``Bc`` are calculated so that the sampled pulse response matches the L1 impulse response. For the FF structure, ``Cc`` is calculated. tdac : sequence, optional The timing for the feedback DAC(s). If ``tdac[0] >= 1``, direct feedback terms are added to the quantizer. Multiple timings (one or more per integrator) for the FB topology can be specified by making tdac a list of lists, e.g. ``tdac = [[1, 2], [1, 2], [[0.5, 1], [1, 1.5]], []]`` In this example, the first two integrators have DACs with ``[1, 2]`` timing, the third has a pair of DACs, one with ``[0.5, 1]`` timing and the other with ``[1, 1.5]`` timing, and there is no direct feedback DAC to the quantizer. ordering : sequence, optional A vector specifying which NTF zero-pair to use in each resonator Default is for the zero-pairs to be used in the order specified in the NTF. bp : sequence, optional A vector specifying which resonator sections are bandpass. The default (``zeros(...)``) is for all sections to be lowpass. ABCDc : ndarray, optional The loop filter structure, in state-space form. If this argument is omitted, ABCDc is constructed according to "form." **Returns:** ABCDc : ndarray A state-space description of the CT loop filter tdac2 : ndarray A matrix with the DAC timings, including ones that were automatically added. **Example:** Realize the NTF :math:`(1 - z^{-1})^2` with a CT system (cf with the example at :func:`mapCtoD`).:: from deltasigma import * ntf = ([1, 1], [0, 0], 1) ABCDc, tdac2 = realizeNTF_ct(ntf, 'FB') Returns: ABCDc:: [[ 0. 0. 1. -1. ] [ 1. 0. 0. -1.49999999] [ 0. 1. 0. 0. ]] tdac2:: [[-1. -1.] [ 0. 1.]] """ ntf_z, ntf_p, _ = _get_zpk(ntf) ntf_z = carray(ntf_z) ntf_p = carray(ntf_p) order = max(ntf_p.shape) order2 = int(np.floor(order/2.)) odd = order - 2*order2 # compensate for limited accuracy of zero calculation ntf_z[np.abs(ntf_z - 1) < eps**(1./(1. + order))] = 1. # check if multiple timings mode if (type(tdac) == list or type(tdac) == tuple) and len(tdac) and \ (type(tdac[0]) == list or type(tdac[0]) == tuple): if len(tdac) != order + 1: msg = 'For multi-timing tdac, len(tdac) ' + \ ' must be order+1.' raise ValueError(msg) if form != 'FB': msg = "Currently only supporting form='FB' " + \ 'for multi-timing tdac' raise ValueError(msg) multi_timing = True else: # single timing tdac = carray(tdac) if np.prod(tdac.shape) != 2: msg = 'For single-timing tdac, len(tdac) must be 2.' raise ValueError(msg) tdac.reshape((2,)) multi_timing = False if ordering is None: ordering = np.arange(order2) if bp is None: bp = np.zeros((order2,)) if not multi_timing: # Need direct terms for every interval of memory in the DAC n_direct = np.ceil(tdac[1]) - 1 if tdac[0] > 0 and tdac[0] < 1 and tdac[1] > 1 and tdac[1] < 2: n_extra = n_direct - 1 # tdac pulse spans a sample point else: n_extra = n_direct tdac2 = np.vstack( (np.array((-1, -1)), np.array(tdac).reshape((1, 2)), 0.5*np.dot(np.ones((n_extra, 1)), np.array([[-1, 1]])) + np.cumsum(np.ones((n_extra, 2)), 0) + (n_direct - n_extra) )) else: n_direct = 0 n_extra = 0 if ABCDc is None: ABCDc = np.zeros((order + 1, order + 2)) # Stuff the A portion if odd: ABCDc[0, 0] = np.real(np.log(ntf_z[0])) ABCDc[1, 0] = 1 dline = np.array([0, 1, 2]) for i in range(order2): n = bp[i] i1 = 2*i + odd zi = 2*ordering[i] + odd w = np.abs(np.angle(ntf_z[zi])) ABCDc[i1 + dline, i1] = np.array([0, 1, n]) ABCDc[i1 + dline, i1 + 1] = np.array([-w**2, 0, 1 - n]) ABCDc[0, order] = 1 # 2006.10.02 Changed to -1 to make FF STF have +ve gain at DC ABCDc[0, order + 1] = -1 Ac = ABCDc[:order, :order] if form == 'FB': Cc = ABCDc[order, :order].reshape((1, -1)) if not multi_timing: Bc = np.hstack((np.eye(order), np.zeros((order, 1)))) Dc = np.hstack((np.zeros((1, order)), np.array([[1]]))) tp = np.tile(np.array(tdac).reshape((1, 2)), (order + 1, 1)) else: #Assemble tdac2, Bc and Dc tdac2 = np.array([[-1, -1]]) Bc = None Dc = None Bci = np.hstack((np.eye(order), np.zeros((order, 1)))) Dci = np.hstack((np.zeros((1, order)), np.array([[1]]))) for i in range(len(tdac)): tdi = tdac[i] if (type(tdi) in (tuple, list)) and len(tdi) and \ (type(tdi[0]) in (list, tuple)): for j in range(len(tdi)): tdj = tdi[j] tdac2 = np.vstack((tdac2, np.array(tdj).reshape(1,-1))) if Bc is not None: Bc = np.hstack((Bc, Bci[:, i].reshape((-1, 1)))) else: Bc = Bci[:, i].reshape((-1, 1)) if Dc is not None: Dc = np.hstack((Dc, Dci[:, i].reshape((-1, 1)))) else: Dc = Dci[:, i].reshape((-1, 1)) elif len(tdi): # we got tdac[i] = [a, b] where a, b are scalars tdac2 = np.vstack((tdac2, np.array(tdi).reshape(1,-1))) if Bc is not None: Bc = np.hstack((Bc, Bci[:, i].reshape((-1, 1)))) else: Bc = Bci[:, i].reshape((-1, 1)) if Dc is not None: Dc = np.hstack((Dc, Dci[:, i].reshape((-1, 1)))) else: Dc = Dci[:, i].reshape((-1, 1)) tp = tdac2[1:, :] elif form == 'FF': Cc = np.vstack((np.eye(order), np.zeros((1, order)))) Bc = np.vstack((np.array([[-1]]), np.zeros((order-1, 1)))) Dc = np.vstack((np.zeros((order, 1)), np.array([[1]]))) tp = tdac # 2008-03-24 fix from Ayman Shabra else: raise ValueError('Sorry, no code for form "%s".', form) # Sample the L1 impulse response n_imp = np.ceil(2*order + np.max(tdac2[:, 1]) + 1) y = impL1(ntf, n_imp) sys_c = [] for i in range(Bc.shape[1]): # number of inputs sys_tmp = [] for j in range(Cc.shape[0]): # number of outputs sys_tmp.append(ss2zpk(Ac, Bc, Cc[j, :], Dc[j, :], input=i)) sys_c.append(sys_tmp) yy = pulse(sys_c, tp, 1, n_imp, 1) yy = np.squeeze(yy) # Endow yy with n_extra extra impulses. # These will need to be implemented with n_extra extra DACs. # !! Note: if t1=int, matlab says pulse(sys) @t1 ~=0 # !! This code corrects this problem. if n_extra > 0: y_right = padb(np.vstack((np.zeros((1, n_direct)), np.eye(n_direct))), n_imp + 1) # Replace the last column in yy with an ordered set of impulses if (n_direct > n_extra): yy = np.hstack((yy, y_right[:, 1:])) else: yy = np.hstack((yy[:, :-1], y_right)) # Solve for the coefficients x = linalg.lstsq(yy, y)[0] if linalg.norm(np.dot(yy, x) - y) > 0.0001: warn('Pulse response fit is poor.') if form == 'FB': if not multi_timing: Bc2 = np.hstack((x[:order].reshape((-1, 1)), np.zeros((order, n_extra)))) if n_extra > 0: Dc2 = np.hstack((np.array([[0]]), x[order:].reshape((-1, 1)))) else: Dc2 = x[order:].reshape((-1, 1)) else: BcDc = np.vstack((Bc, Dc)) i = np.nonzero(BcDc) BcDc[i] = x Bc2 = BcDc[:-1, :] Dc2 = BcDc[-1, :] elif form == 'FF': Bc2 = np.hstack((Bc, np.zeros((order, n_extra)))) Cc = x[:order].reshape((1, -1)) if n_extra > 0: Dc2 = np.hstack((np.array([[0]]), x[order:].T)) else: Dc2 = x[order:].T Dc1 = np.zeros((1, 1)) Dc = np.hstack((Dc1, np.atleast_2d(Dc2))) Bc1 = np.vstack((np.ones((1, 1)), np.zeros((order - 1, 1)))) Bc = np.hstack((Bc1, Bc2)) # Scale Bc1 for unity STF magnitude at f0 fz = np.angle(ntf_z)/(2*np.pi) f1 = fz[0] ibz = np.abs(fz - f1) <= np.abs(fz + f1) fz = fz[ibz] f0 = np.mean(fz) if np.min(np.abs(fz)) < 3*np.min(np.abs(fz - f0)): f0 = 0 L0c = ss2zpk(Ac, Bc1, Cc, Dc1) G0 = evalTFP(L0c, ntf, f0) if f0 == 0: Bc[:, 0] = np.dot(Bc[:, 0], np.abs(np.dot(Bc[0, 1:], (tdac2[1:, 1] - tdac2[1:, 0])) /Bc[0, 0])) else: Bc[:, 0] = Bc[:, 0]/np.abs(G0) ABCDc = np.vstack(( np.hstack((Ac, Bc)), np.hstack((Cc, Dc)) )) #ABCDc = np.dot(ABCDc, np.abs(ABCDc) > eps**(1./2.)) ABCDc[np.nonzero(np.abs(ABCDc) < eps**(1./2))] = 0. return ABCDc, tdac2
def calculateTF(ABCD, k=1.): """Calculate the NTF and STF of a delta-sigma modulator. The calculation is performed for a given loop filter ABCD matrix, assuming a quantizer gain of ``k``. **Parameters:** ABCD : array_like, The ABCD matrix that describes the system. k : float or ndarray-like, optional The quantizer gains. If only one quantizer is present, it may be set to a float, corresponding to the quantizer gain. If multiple quantizers are present, a list should be used, with quantizer gains ordered according to the order in which the quantizer inputs appear in the ``C`` and ``D`` submatrices. If not specified, a default of one quantizer with gain ``1.`` is assumed. **Returns:** (NTF, STF) : a tuple of two LTI objects (or of two lists of LTI objects). If a version of the ``scipy`` library equal to 0.16.x or greater is in use, the objects will be ``ZeroPolesGain`` objects, a subclass of ``scipy.signal.lti``. If the system has multiple quantizers, multiple STFs and NTFs will be returned. In that case: * ``STF[i]`` is the STF from ``u`` to output number ``i``. * ``NTF[i, j]`` is the NTF from the quantization noise of the quantizer number ``j`` to output number ``i``. **Note:** Setting ``k`` to a list is unsupported in the MATLAB code (last checked Nov. 2014). **Example:** Realize a fifth-order modulator with the cascade-of-resonators structure, feedback form. Calculate the ABCD matrix of the loop filter and verify that the NTF and STF are correct. .. code-block:: python from deltasigma import * H = synthesizeNTF(5, 32, 1) a, g, b, c = realizeNTF(H) ABCD = stuffABCD(a,g,b,c) ntf, stf = calculateTF(ABCD) From which we get: ``H``:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) coefficients:: a: 0.0007, 0.0084, 0.055, 0.2443, 0.5579 g: 0.0028, 0.0079 b: 0.0007, 0.0084, 0.055, 0.2443, 0.5579, 1.0 c: 1.0, 1.0, 1.0, 1.0, 1.0 ABCD matrix:: [[ 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.75559806e-04 -6.75559806e-04] [ 1.00000000e+00 1.00000000e+00 -2.79396240e-03 0.00000000e+00 0.00000000e+00 8.37752565e-03 -8.37752565e-03] [ 1.00000000e+00 1.00000000e+00 9.97206038e-01 0.00000000e+00 0.00000000e+00 6.33294166e-02 -6.33294166e-02] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 -7.90937431e-03 2.44344030e-01 -2.44344030e-01] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 9.92090626e-01 8.02273699e-01 -8.02273699e-01] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00]] NTF:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) STF:: 1 """ nq = len(k) if type(k) in (tuple, list) else 1 A, B, C, D = partitionABCD(ABCD, m=nq+1, r=nq) k = carray(k) diagk = np.atleast_2d(np.diag(k)) B1 = B[:, 0] B2 = B[:, 1:] B1 = B1.reshape((B1.shape[0], 1)) if len(B1.shape) == 1 else B1 B2 = B2.reshape((B2.shape[0], 1)) if len(B2.shape) == 1 else B2 # In a single-quantizer system, D2 should be all zeros, as any # non-zero term in D2 would imply we got a delay-free loop. # The original MATLAB code implies so. # The trouble arises when we adapt and extend the code to calculate # the transfer functions for multiple-quantizer systems. There the # off-diagonal terms of D2 may very well be non-zero, therefore # in the following we consider D2. # this means that if you supply an ABCD matrix, with one quantizer # and an (erroneously) zero-delay loop, the MATLAB toolbox will # disregard D2 and pretend it's zero and the loop is correctly # delayed, spitting out the corresponding TF. # Instead here we print out a warning and process the ABCD matrix # with the user-supplied, non-zero D2 matrix. # The resulting TFs will obviously be different. D1 = D[:, 0] D2 = D[:, 1:] D1 = D1.reshape((D1.shape[0], 1)) if len(D1.shape) == 1 else D1 D2 = D2.reshape((D2.shape[0], 1)) if len(D2.shape) == 1 else D2 # WARN DELAY FREE LOOPS if np.diag(D2).any(): warn("Delay free loop detected! D2 diag: %s", str(np.diag(D2))) # Find the noise transfer function by forming the closed-loop # system (sys_cl) in state-space form. Ct = np.linalg.inv(np.eye(nq) - D2*diagk) Acl = A + np.dot(B2, np.dot(Ct, np.dot(diagk, C))) Bcl = np.hstack((B1 + np.dot(B2, np.dot(Ct, np.dot(diagk, D1))), np.dot(B2, Ct))) Ccl = np.dot(Ct, np.dot(diagk, C)) Dcl = np.dot(Ct, np.hstack((np.dot(diagk, D1), np.eye(nq)))) tol = min(1e-3, max(1e-6, eps**(1/ABCD.shape[0]))) ntfs = np.empty((nq, Dcl.shape[0]), dtype=np.object_) stfs = np.empty((Dcl.shape[0],), dtype=np.object_) # sweep the outputs 'cause scipy is silly but we love it anyhow. for i in range(Dcl.shape[0]): # input #0 is the signal # inputs #1,... are quantization noise stf_z, stf_p, stf_k = ss2zpk(Acl, Bcl, Ccl[i, :], Dcl[i, :], input=0) stf = lti(stf_z, stf_p, stf_k) for j in range(nq): ntf_z, ntf_p, ntf_k = ss2zpk(Acl, Bcl, Ccl[i, :], Dcl[i, :], input=j+1) ntf = lti(ntf_z, ntf_p, ntf_k) stf_min, ntf_min = minreal((stf, ntf), tol) ntfs[i, j] = ntf_min stfs[i] = stf_min # if we have one stf and one ntf, then just return those in a list if ntfs.shape == (1, 1): return [ntfs[0, 0], stfs[0]] return ntfs, stfs
def calculateTF(ABCD, k=1.): """Calculate the NTF and STF of a delta-sigma modulator. The calculation is performed for a given loop filter ABCD matrix, assuming a quantizer gain of ``k``. **Parameters:** ABCD : array_like, The ABCD matrix that describes the system. k : float, optional The quantizer gain. If not specified, a default value of 1 is used. **Returns:** (NTF, STF) : a tuple of two LTI objects. **Example:** Realize a fifth-order modulator with the cascade-of-resonators structure, feedback form. Calculate the ABCD matrix of the loop filter and verify that the NTF and STF are correct. .. code-block:: python from deltasigma import * H = synthesizeNTF(5, 32, 1) a, g, b, c = realizeNTF(H) ABCD = stuffABCD(a,g,b,c) ntf, stf = calculateTF(ABCD) From which we get: ``H``:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) coefficients:: a: 0.0007, 0.0084, 0.055, 0.2443, 0.5579 g: 0.0028, 0.0079 b: 0.0007, 0.0084, 0.055, 0.2443, 0.5579, 1.0 c: 1.0, 1.0, 1.0, 1.0, 1.0 ABCD matrix:: [[ 1.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 6.75559806e-04 -6.75559806e-04] [ 1.00000000e+00 1.00000000e+00 -2.79396240e-03 0.00000000e+00 0.00000000e+00 8.37752565e-03 -8.37752565e-03] [ 1.00000000e+00 1.00000000e+00 9.97206038e-01 0.00000000e+00 0.00000000e+00 6.33294166e-02 -6.33294166e-02] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 -7.90937431e-03 2.44344030e-01 -2.44344030e-01] [ 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 9.92090626e-01 8.02273699e-01 -8.02273699e-01] [ 0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00 1.00000000e+00 1.00000000e+00 0.00000000e+00]] NTF:: (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999) -------------------------------------------------------- (z -0.7778) (z^2 -1.796z +0.8549) (z^2 -1.613z +0.665) STF:: 1 """ A, B, C, D = partitionABCD(ABCD) if B.shape[1] > 1: B1 = B[:, 0] B2 = B[:, 1] B1 = B1.reshape((B1.shape[0], 1)) if len(B1.shape) == 1 else B1 B2 = B2.reshape((B2.shape[0], 1)) if len(B2.shape) == 1 else B2 else: B1 = B B2 = B # Find the noise transfer function by forming the closed-loop # system (sys_cl) in state-space form. Acl = A + k * np.dot(B2, C) Bcl = np.hstack((B1 + k * B2 * D[0, 0], B2)) Ccl = k * C Dcl = np.array((k * D[0, 0], 1.)) Dcl = Dcl.reshape((1, Dcl.shape[0])) if len(Dcl.shape) == 1 else Dcl tol = min(1e-3, max(1e-6, eps**(1 / ABCD.shape[0]))) # input #0 is the signal # input #1 is the quantization noise stf_p, stf_z, stf_k = ss2zpk(Acl, Bcl, Ccl, Dcl, input=0) ntf_p, ntf_z, ntf_k = ss2zpk(Acl, Bcl, Ccl, Dcl, input=1) stf = lti(stf_p, stf_z, stf_k) ntf = lti(ntf_p, ntf_z, ntf_k) stf_min, ntf_min = minreal((stf, ntf), tol) return ntf_min, stf_min
_sys2ss = { _LSYS: lambda sys: sys.ss, _LFILT: lambda sys: tf2ss(sys.num, sys.den), _NUM: lambda sys: tf2ss(sys, 1), _TF: lambda sys: tf2ss(*sys), _ZPK: lambda sys: zpk2ss(*sys), _SS: lambda sys: sys, } _sys2zpk = { _LSYS: lambda sys: sys.zpk, _LFILT: lambda sys: tf2zpk(sys.num, sys.den), _NUM: lambda sys: tf2zpk(sys, 1), _TF: lambda sys: tf2zpk(*sys), _ZPK: lambda sys: sys, _SS: lambda sys: ss2zpk(*sys), } _sys2tf = { _LSYS: lambda sys: sys.tf, _LFILT: lambda sys: _tf(sys.num, sys.den), _NUM: lambda sys: _tf(sys, 1), _TF: lambda sys: _tf(*sys), _ZPK: lambda sys: _tf(*zpk2tf(*sys)), _SS: lambda sys: _tf(*_ss2tf(*sys)), } def sys2ss(sys): """Converts an LTI system in any form to state-space.""" return _sys2ss[_sys2form(sys)](sys)