# -*- coding: utf-8 -*-
# _clans.py
# Closed-Loop Analysis of Noise-Shapers module
# Copyright 2013 Giuseppe Venturini
# This file is part of python-deltasigma.
#
# python-deltasigma is a 1:1 Python replacement of Richard Schreier's 
# MATLAB delta sigma toolbox (aka "delsigma"), upon which it is heavily based.
# The delta sigma toolbox is (c) 2009, Richard Schreier.
#
# python-deltasigma is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# LICENSE file for the licensing terms.

"""Module providing the clans() optimization function.
"""

from __future__ import division, print_function

import numpy as np
from scipy.optimize import minimize
from scipy.signal import dimpulse

from ._dsclansNTF import dsclansNTF
from ._evalTF import evalTF
from ._synthesizeNTF import synthesizeNTF
from ._utils import cplxpair


# We ported the version that was originally designed for the MATLAB
# Optimization Toolbox version >= 6

def clans(order=4, OSR=64, Q=5, rmax=0.95, opt=0):
    """Optimal NTF design for a multi-bit modulator.
    
    Synthesize a noise transfer function (NTF) for a lowpass delta-sigma 
    modulator using the CLANS methodology.

    CLANS stands for Closed-Loop Analysis of Noise-Shapers,
    and it was originally developed by J.G. Kenney and L.R. Carley [1]_.

    .. [1] J. G. Kenney and L. R. Carley, “Design of multibit
        noise-shaping data converters,” Analog Integrated Circuits Signal
        Processing Journal, vol. 3, pp. 259-272, 1993.

    **Parameters:**

    order : int
        The order of the NTF.

    OSR : int
        The oversampling ratio.

    Q : int
        The maximum number of quantization levels used by the fed-back quantization
        noise. (Mathematically, :math:`Q = \\|h\\|_1 - 1`, i.e. the sum of the 
        absolute values of the impulse response samples minus one is the 
        maximum instantaneous noise gain.)

    rmax : float
        The maximum radius for the NTF poles.

    opt : int
        A flag used to request optimized NTF zeros. 
        
    * `opt=0` puts all NTF zeros at band center (DC for lowpass modulators).
    * `opt=1` optimizes the NTF zeros.
    * For even-order modulators, `opt=2` puts two zeros at band-center, 
      but optimizes the rest.

    **Returns**

    ntf : tuple
        The modulator NTF, given in ZPK (zero-pole-gain) form.

    **Example:**

    Fifth-order lowpass modulator; (time-domain) noise gain of 5, 
    zeros optimized for OSR = 32.::

        H = clans(5, 32, 5, .95, 1)
        pretty_lti(H)

    Returns::

              (z -1) (z^2 -1.997z +1) (z^2 -1.992z +0.9999)      
        ---------------------------------------------------------
         (z -0.4184) (z^2 -1.305z +0.5713) (z^2 -0.978z +0.2686) 

    ``H`` can be plotted through :func:`DocumentNTF`:

    .. plot::

        from deltasigma import DocumentNTF, clans
        # Fifth-order lowpass modulator; (time-domain) noise gain of 5, 
        # zeros optimized for OSR = 32.
        H = clans(5, 32, 5, .95, 1)
        DocumentNTF(H)

    """
    # Create the initial guess
    Hz, poles, _ = synthesizeNTF(order, OSR, opt, 1 + Q, 0)
    x = np.zeros((order, ))
    odd = order % 2
    poles = cplxpair(poles)
    poles = poles[::-1]
    if odd == 1:
        z = poles[0]/rmax
        if (np.abs(z) > 1).any(): #project poles outside rmax onto the circle
            z = z/np.abs(z)
        s = (z - 1)/(z + 1)
        x[0] = np.real_if_close(np.sqrt(-s))
    for i in range(odd, order, 2):
        z = poles[i:i + 2]/rmax
        if np.any(np.abs(z) > 1): #project poles outside rmax onto the circle
            z = z/np.abs(z)
        s = (z - 1)/(z + 1)
        coeffs = np.poly(s)
        wn = np.sqrt(coeffs[2])
        zeta = coeffs[1]/(2 * wn)
        x[i] = np.real_if_close(np.sqrt(zeta))
        x[i + 1] = np.real_if_close(np.sqrt(wn))
    #options=optimset('TolX',1e-06,'TolFun',1e-06,'TolCon',1e-06,'MaxIter',1000)
    #fobj = lambda x: dsclansObj6a(x, order, OSR, Q, rmax, Hz)
    #fconstr = lambda x: dsclansObj6b(x, order, OSR, Q, rmax, Hz)
    res = minimize(dsclansObja, x, args=(order, OSR, Q, rmax, Hz), 
             method='slsqp', constraints={'type':'ineq', 'fun':dsclansObjb, 
             'args':(order, OSR, Q, rmax, Hz)})
    NTF = dsclansNTF(res['x'], order, rmax, Hz)
    return NTF

def dsclansObja(x, order, OSR, Q, rmax, Hz):
    """Objective function for clans; Optimization Toolbox version >= 6
    f is the magnitude of H at the band-edge
    """
    H = dsclansNTF(x, order, rmax, Hz)
    f = np.abs(evalTF(H, np.exp(1j*np.pi/OSR)))
    return f

def dsclansObjb(x, order, OSR, Q, rmax, Hz):
    """Constraint function for clans; 
    g =||h||_1 - Q
    """
    H = dsclansNTF(x, order, rmax, Hz)
    H = (H[0], H[1], H[2], 1.)
    # dimpulse(H, n=100)[y is 0][output 0]
    g = np.sum(np.abs(dimpulse(H, t=np.arange(100))[1][0])) - 1 - Q
    return -g
