#!/usr/bin/env python
# coding: utf-8

# TODO:
#
#
# R1:
# - get in touch with ngspice developers to go over xspice's lcouple & magnetic core
# - reivew the tcoil more in depth
#
#
# R2:
# - Get rest of standard transformer done
#

# In[1]:

from skidl.pyspice import *
#can you say cheeky
import PySpice as pspice
#becouse it's written by a kiwi you know
import lcapy as kiwi

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import sympy as sym

from scipy.signal import zpk2tf as scipy_zpk2tf

from IPython.display import YouTubeVideo, display

import traceback
import warnings

# In[2]:

#import dc code from parral folder
import sys
sys.path.insert(1, '../DC_1/')
from DC_1_Codes import get_skidl_spice_ref

from AC_2_Codes import *

sym.init_printing()

#notebook specific loading control statements
get_ipython().run_line_magic('matplotlib', 'inline')
#tool to log notebook internals
#https://github.com/jrjohansson/version_information
get_ipython().run_line_magic('load_ext', 'version_information')
get_ipython().run_line_magic('version_information', 'skidl, PySpice,lcapy, sympy, numpy, matplotlib, pandas, scipy')

# # Mutual Inductance and SPICE
#
# For a recollection of the basic theory of self and mutual inductance recall the YT video on the subject below from ALL ABOUT ELECTRONICS. However, let's be clear about this right now; SPICE is not a field simulator. SPICE just does a bit more than your EE 101/102 textbook. Though we will see there are ways to enhance the basic SPICE with XSPICE to implement more advanced magnetic device models. But even with XSPICE, we can still not get to the level of field solvers like Ansys who will do full E&M simulations and then extract the S-parameters and then use that as part of its SPICE incarnation.
#

# In[3]:

YouTubeVideo('hoTInTKij0o', width=500, height=400)

# The other thing to recall is that inductors in reference to other inductors are polarized elements and that polarization does have consequences as explained by ALL ABOUT ELECTRONICS in the YT video "Dot Convention in Magnetically Coupled Circuits"

# In[4]:

YouTubeVideo('sILgO4sQmRs', width=500, height=400)

# Where in SPICE as we will see the dot belongs to the first terminal of the inductor. Further, to make SPICE aware of what inductors are coupled to which we use the `K` statement who’s SPICE syntax is
#
# ```
# KXXXXXXX LYYYYYYY LZZZZZZZ value
# ```
#
# Where `KXXXXXXX` is the name of the coupling, `LYYYYYYY` and `LZZZZZZZ` are the two inductors that are being coupled. And finally, value the value of the coefficient of coupling $k$ where $k$ can take on a value from 0 to 1 and is defined as
#
# $$k=\dfrac{M}{\sqrt{L_Y L_Z}}$$
#
# where $M$ is the mutual inductance between the two inductors and $L_Y$ & $L_Z$ are the values of the inductors of the two inductors respectively
#

# # Series Mutual inductance

# The following is a quick testbench to when testing mutual inductance in SPICE

# In[5]:

#%%writefile -a AC_2_Codes.py
#chapteer 2 section 3 mutual_tester class
#class to perform the analysis of the coupled inductors in section 3

class mutual_tester(ac_ease, ac_representation_tool, eecomplex_plot_templets):
    """
    Quick class to test mutual inductance
    """

    def __init__(self, circ, title, start_freq=1@u_Hz, stop_freq=100@u_Hz):
        """
        Quick class to test mutual inductance

        Args:
            circ (pyspice circuit): circuit under test
            title (str): title for plots
            start_freq(float; Hertz; 1@u_Hz): the starting freauncy
            stop_freq(float; Hertz; 100@u_Hz): the ending freauncy

        Returns:
            does the ac sim of the circuit, produces the ac values transformed
            in `self.ac_sim_mag_DF` ect and creates a bode plot of the results

        """

        #do what ac_ease is supposed to do at startup
        #instainte the simulation from the circuit
        ac_ease.__init__(self, circ)
        #setup the simulation parameter with the helper method
        self.ac_sweep_setup(start_freq, stop_freq, 20, 'decade', True)
        #do the simulation
        self.do_ac_sim()

        #do what ac_representation_tool is supposed to do at startup
        #and pass in the selfs from `ac_ease`'s ac_resultsNB_DF
        ac_representation_tool.__init__(self, self.ac_resultsNB_DF)
        #generate the representations
        self.make_mag_phase()

        eecomplex_plot_templets.__init__(self)

        fig, ax_bode=plt.subplots(nrows=1, ncols=1)

        self.bode_plot_one_templet(self.ac_sim_mag_DF.index, self.ac_sim_mag_DF['V1_[A][dB]'], self.ac_sim_phase_DF['V1_[A][deg]'],
                          title='Source_Current_Draw', ax=ax_bode)

        fig.suptitle(title)

# For each of the examples from ALL ABOUT ELECTRONICS here will create the circuit in three ways to see what effect mutual coupling has. The first is with no coupling. The second is invoking the mutual coupling. And the third case implementing the circuit with the theoretical simplification. This is followed by plotting all three cases on top of each other to see the effect of invoking mutual coupling and the theoretical simplification from SPICE’s view.

# ## Series Adding Mutual Inductance from ALL ABOUT ELECTRONICS "Mutually Coupled Inductors in Series (Derivation / Proof and Examples)" @~ 6:04min

# In[6]:

YouTubeVideo('49OL_3L7BFA', width=500, height=400, start=364)

# ### No Coupling

# In[7]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=3@u_H)
l2=L(value=3@u_H)
net_in & l1[1, 2] & l2[1, 2] & gnd

#k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_nok=generate_netlist()
print(circ_nok)

# In[8]:

anylsis_nok=mutual_tester(circ_nok, 'No Coupling')

# ### With Coupling

# In[9]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=3@u_H)
l2=L(value=3@u_H)
net_in & l1[1, 2] & l2[1, 2] & gnd

k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_k=generate_netlist()
print(circ_k)

# In[10]:

anylsis_k=mutual_tester(circ_k, 'With Coupling')

# ### Equivlint Single Inductor

# In[11]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=9@u_H)
net_in & l1[1, 2] & gnd

circ_eq=generate_netlist()
print(circ_eq)

# In[12]:

anylsis_eq=mutual_tester(circ_eq, 'Equivalent')

# ### Comperion between all three cases

# In[13]:

comp_plot=eecomplex_plot_templets()
fig, [ax_mag, ax_phase]=plt.subplots(ncols=1, nrows=2)
comp_plot.bode_plot_two_templet(anylsis_nok.ac_sim_mag_DF.index,
                               anylsis_nok.ac_sim_mag_DF['V1_[A][dB]'],
                               anylsis_nok.ac_sim_phase_DF['V1_[A][deg]'],
                        axs=[ax_mag, ax_phase]
                               )
ax_mag.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_mag_DF['V1_[A][dB]'], label='mitK')
ax_phase.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_phase_DF['V1_[A][deg]'], label='mitK')

ax_mag.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_mag_DF['V1_[A][dB]'], alpha=.4, label="eq")
ax_phase.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_phase_DF['V1_[A][deg]'], alpha=.4, label='eq')

ax_mag.legend()
ax_phase.legend()
plt.suptitle('Compersion Bode plots of Serial Inductors')
plt.tight_layout();

# ## Series Opposing Mutual Inductance from ALL ABOUT ELECTRONICS "Mutually Coupled Inductors in Series (Derivation / Proof and Examples)" @~ 7:17min

# In[14]:

YouTubeVideo('49OL_3L7BFA', width=500, height=400, start=437)

# ### No Coupling

# In[15]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
net_in & l1[1, 2] & l2[2, 1] & gnd

#k=K(ind1=l1, ind2=l2, coupling=0.9)

circ_nok=generate_netlist()
print(circ_nok)

# In[16]:

anylsis_nok=mutual_tester(circ_nok, 'No Coupling')

# ### With Coupling

# In[17]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
net_in & l1[1, 2] & l2[2, 1] & gnd

k=K(ind1=l1, ind2=l2, coupling=0.9)

circ_k=generate_netlist()
print(circ_k)

# In[18]:

anylsis_k=mutual_tester(circ_k, 'With Coupling')

# ### Equivalent Single Inductor

# In[19]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1.4@u_H)
net_in & l1[1, 2] & gnd

circ_eq=generate_netlist()
print(circ_eq)

# In[20]:

anylsis_eq=mutual_tester(circ_eq, 'Equivalent')

# ### Compersion between all three cases

# In[21]:

comp_plot=eecomplex_plot_templets()
fig, [ax_mag, ax_phase]=plt.subplots(ncols=1, nrows=2)
comp_plot.bode_plot_two_templet(anylsis_nok.ac_sim_mag_DF.index,
                               anylsis_nok.ac_sim_mag_DF['V1_[A][dB]'],
                               anylsis_nok.ac_sim_phase_DF['V1_[A][deg]'],
                        axs=[ax_mag, ax_phase]
                               )
ax_mag.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_mag_DF['V1_[A][dB]'], label='mitK')
ax_phase.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_phase_DF['V1_[A][deg]'], label='mitK')

ax_mag.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_mag_DF['V1_[A][dB]'], alpha=.4, label="eq")
ax_phase.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_phase_DF['V1_[A][deg]'], alpha=.4, label='eq')

ax_mag.legend()
ax_phase.legend()
plt.suptitle('Compersion Bode plots of Anti-Serial Inductors')
plt.tight_layout();

# # Parallel Mutual Inductance

# ## Parallel Adding Mutual Inductance from ALL ABOUT ELECTRONICS "Mutually Coupled Inductors in Parallel (Derivation / Proof and Examples)" @~ 13:04min

# In[22]:

YouTubeVideo('NacczEJj_iI', width=500, height=400, start=784)

# ### No Coupling

# In[23]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
rdummy2=R(ref='dummy', value=0@u_Ohm)

l1[1, 2]+=net_in, gnd
rdummy2[1, 2]+=l1[1], l2[1]
l2[2]+=gnd

#k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_nok=generate_netlist()
print(circ_nok)

# In[24]:

anylsis_nok=mutual_tester(circ_nok, 'No Coupling')

# ### With Coupling

# In[25]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
rdummy2=R(ref='dummy', value=0@u_Ohm)

l1[1, 2]+=net_in, gnd
rdummy2[1, 2]+=l1[1], l2[1]
l2[2]+=gnd

k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_k=generate_netlist()
print(circ_k)

# In[26]:

anylsis_k=mutual_tester(circ_k, 'With Coupling')

# ### Equivalent Single Inductor

# In[27]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
net_in & l1[1, 2] & gnd

#k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_eq=generate_netlist()
print(circ_eq)

# In[28]:

anylsis_eq=mutual_tester(circ_eq, 'Equivalent Parallel Inductors')

# ### Compersion between all three cases

# In[29]:

comp_plot=eecomplex_plot_templets()
fig, [ax_mag, ax_phase]=plt.subplots(ncols=1, nrows=2)
comp_plot.bode_plot_two_templet(anylsis_nok.ac_sim_mag_DF.index,
                               anylsis_nok.ac_sim_mag_DF['V1_[A][dB]'],
                               anylsis_nok.ac_sim_phase_DF['V1_[A][deg]'],
                        axs=[ax_mag, ax_phase]
                               )
ax_mag.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_mag_DF['V1_[A][dB]'], label='mitK')
ax_phase.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_phase_DF['V1_[A][deg]'], label='mitK')

ax_mag.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_mag_DF['V1_[A][dB]'], alpha=.4, label="eq")
ax_phase.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_phase_DF['V1_[A][deg]'], alpha=.4, label='eq')

ax_mag.legend()
ax_phase.legend()
plt.suptitle('Compersion Bode plots of Parallel Inductors')
plt.tight_layout();

# ## Parallel Opposing Mutual Inductance from ALL ABOUT ELECTRONICS "Mutually Coupled Inductors in Parallel (Derivation / Proof and Examples)" @~ 14:25min

# In[30]:

YouTubeVideo('NacczEJj_iI', width=500, height=400, start=865)

# ### No Coupling

# In[31]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
rdummy2=R(ref='dummy', value=0@u_Ohm)

l1[1, 2]+=net_in, gnd
rdummy2[1, 2]+=l1[1], l2[2]
l2[1]+=gnd

#k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_nok=generate_netlist()
print(circ_nok)

# In[32]:

anylsis_nok=mutual_tester(circ_nok, 'Anti-Parallel Inductor with No Coupling')

# ### With Coupling

# In[33]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
l2=L(value=4@u_H)
rdummy2=R(ref='dummy', value=0@u_Ohm)

l1[1, 2]+=net_in, gnd
rdummy2[1, 2]+=l1[1], l2[2]
l2[1]+=gnd
k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_k=generate_netlist()
print(circ_k)

# In[34]:

anylsis_k=mutual_tester(circ_k, 'Anti-Parallel Inductors with Coupling')

# ### Equivalent Single Inductor

# In[35]:

reset()
#create the nets
net_in=Net('In')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

l1=L(value=1@u_H)
net_in & l1[1, 2] & gnd

#k=K(ind1=l1, ind2=l2, coupling=0.5)

circ_eq=generate_netlist()
print(circ_eq)

# In[36]:

anylsis_eq=mutual_tester(circ_eq, 'Equivalent Anti-Parallel Inductor')

# ### Compersion between all three cases

# In[37]:

#compare without K, with K, theoretical equivalent
comp_plot=eecomplex_plot_templets()
fig, [ax_mag, ax_phase]=plt.subplots(ncols=1, nrows=2)
comp_plot.bode_plot_two_templet(anylsis_nok.ac_sim_mag_DF.index,
                               anylsis_nok.ac_sim_mag_DF['V1_[A][dB]'],
                               anylsis_nok.ac_sim_phase_DF['V1_[A][deg]'],
                        axs=[ax_mag, ax_phase]
                               )
ax_mag.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_mag_DF['V1_[A][dB]'], label='mitK')
ax_phase.semilogx(anylsis_k.ac_sim_mag_DF.index, anylsis_k.ac_sim_phase_DF['V1_[A][deg]'], label='mitK')

ax_mag.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_mag_DF['V1_[A][dB]'], alpha=.4, label="eq")
ax_phase.semilogx(anylsis_eq.ac_sim_mag_DF.index, anylsis_eq.ac_sim_phase_DF['V1_[A][deg]'], alpha=.4, label='eq')

ax_mag.legend()
ax_phase.legend()

plt.suptitle('Compersion Bode plots of Anti-Parallel Inductors')
plt.tight_layout();

# # Purposely designed Mutual Inductive Element; The Razivi T-Coil
#
# So, the question that should be asked is when does mutual induction come into play outside of transformers. From a field’s standpoint, mutual induction can be used in testing the effect of placing inductors too close to each other in layout by extracting $k$ from the field simulation. But it does have uses. One of which that shows up a lot in high-speed SERDES is the Razivi T-Coil where a paper on the topology can be found there: [The Bridged T-Coil](https://www.seas.ucla.edu/brweb/papers/Journals/BRFall15TCoil.pdf).
#
# A study of a variation of the T-coil was given by Jordan Edmunds in a two-part YT video on the subject. Where we will recreate the example from part two.
#

# In[38]:

YouTubeVideo('-kSSMOAhlwU', width=500, height=400)

# In[39]:

YouTubeVideo('vr2oYehKC8Q', width=500, height=400)

# In[40]:

#%%writefile -a AC_2_Codes.py
#chapteer 2 section 3 t_coil class
#class with lcapy and skidl subcircuit to create a T-coil from https://youtu.be/vr2oYehKC8Q

class t_coil():
    def __init__(self, subcirc_ref=None, L_value=375@u_nH, K_value=1.0/3.0, CB_value=.125@u_pF, CL_value=1@u_pF):
        """
        T-Coil example from https://youtu.be/vr2oYehKC8Q example

        Args:
            subcirc_ref (str): reference to use for the base of the internal elements
            L_value (float; 375@u_nH; Henerys):  inductance of each of the two inductors in the T-coil
            K_value (float; 1/3): the magnetic coupling between the two inductors
            CB_value (float; .125@u_pF; Farads): capacitance of the bridge capacitor between the two coils
            CL_value (float; 1@u_pF; Farads): capacitance of the shunt capacitor between the two cols

        Returns:
            None
        TODO:
            -add assertions

        """

        self.subcirc_ref=subcirc_ref
        self.L_value=L_value
        self.K_value=K_value
        self.CB_value=CB_value
        self.CL_value=CL_value

    @subcircuit
    def SKiDl(self, term_0, term_1, term_2, term_3, return_elements=False):
        """
        Terminals:
        term_0, term_1, term_2, term_3

        Terminals are defined via:
        ```
        Left_Termanals - T-coil - Right_Termanals
                             +----+
        Postive V_i   term_0-|0  2|-term_2     Postive V_o
        Negtive V_i   term_1-|1  3|-term_3     Negtive V_o
                             +----+
        ```

        Args:
            return_internls (bool; False): If True return out the internal Voltage Source,
                and Resistance objects in this package
        Returns:
            Returns elements to circuit RC lowpass filter part element object and if `return_internls`
            is True will return the internal voltage and resistance objects in that order
        """
        if self.subcirc_ref!=None:
            CBref={'ref':f'CB_{self.subcirc_ref}'}
            CLref={'ref':f'CL_{self.subcirc_ref}'}
            LLref={'ref':f'LL_{self.subcirc_ref}'}
            LRref={'ref':f'LR_{self.subcirc_ref}'}

        else:
            CBref={}
            CLref={}
            LLref={}
            LRref={}

        self.cb=C(value=self.CB_value, **CBref)
        self.cl=C(value=self.CL_value, **CLref)
        self.ll=L(value=self.L_value, **LLref)
        self.lr=L(value=self.L_value, **LRref)
        self.mul=K(ind1=self.ll, ind2=self.lr, coupling=self.K_value)

        term_0 & self.ll[2, 1]  & self.lr[2, 1] & term_2
        self.cb['p', 'n']+=term_0, term_2
        self.cl['p', 'n']+=self.ll[1], term_1
        term_1+=term_3

        if return_elements:
            return self.cb, self.cl, self.ll, self.lr, self.mul

    def lcapy_self(self, draw_me=True, with_values=True):
        """
        Creates a lcapy schematic of this classes filter that
        can be used for amongst other things: draw a basic schematic
        of this class circuit, extract the transfer function,
        extract the 2Port Representation

        Args:
            draw_me (bool): will draw a schematic of this classes circuit schematic

            with_values (bool): will push the filters element values into
                the resulting lcapy circuit object in place of abstract simply variables

        Return:
            the lcapys circuit object is stored in `self.schematic` and will
            have abstract sympy variable for the elements if `with_values` is False
            and will draw the schematic of just this classes filter if `draw_me` is True

        TODO:
            - Figure out how to add the K dots
            - get the Vin statement into the schematic

        """
        self.with_values=with_values

        self.schematic=kiwi.Circuit()
        self.schematic.add('W 1 1_1; right=2')
        self.schematic.add('W 0 0_1; right')

        self.schematic.add('Ll N1 0_1; left')
        self.schematic.add('Lr N1 2_1; right')
        self.schematic.add('Cl N1 1_1; down')

        self.schematic.add('W 0_1 0_2; up')
        self.schematic.add('W 2_1 2_2; up')
        self.schematic.add('Cb 0_2 2_2; right')

        self.schematic.add('W 2_1 2; right=1.5')
        self.schematic.add('W 1_1 3; right=1.5')
        self.schematic.add('P2 2 3; down, v=V_o')

        if with_values:
            self.schematic=self.schematic.subs({
                'Ll':self.L_value,
                'Lr':self.L_value,
                'Cl':self.CL_value,
                'Cb':self.CB_value})

        if draw_me:
            self.schematic.draw()

    def get_tf(self, with_values=True, ZPK=True):
        """
        will extract the symbolic transfer function for this filter

        Args:
            with_values (bool): will push the filters element values into
                the resulting lcapy circuit object in place of abstract sympy variables

            ZPK (bool): if True will try to return the TF in zero-pole-gain form
                else will try to return the TF in canonical form
                where the unity coefficient is the highest power of the denominator

        """
        self.lcapy_self(draw_me=False, with_values=with_values)
        self.tf=self.schematic.transfer(0, 1, 2, 3)

        if ZPK:
            return self.tf.ZPK()
        else:
            return self.tf.canonical()

    def get_twoPort(self, network_rep='Y', with_values=True):
        """
        Gets the 2Port network representation of this filter.
        The 2Port representation can be controlled

        Args:
            network_rep (str; 'Y'): control string for what
                representation is used to choose from:

                'Z': two-port Z-parameters matrix
                'Y': two-port Y-parameters matrix
                'H': two-port H-parameters matrix
                'G': two-port G-parameters matrix
                'ABCD': two-port A-parameters matrix
                'invABCD': two-port B-parameters matrix
                'S': two-port S-parameters matrix
                'T': two-port T-parameters matrix

        """

        self.lcapy_self(draw_me=False, with_values=with_values)

        #create an action dict to get the 2P rep
        rep_actions={
            'Z':lambda x: x.Zparams(0, 1, 2, 1),
            'Y':lambda x: x.Yparams(0, 1, 2, 1),
            'H':lambda x: x.Hparams(0, 1, 2, 1),
            'G':lambda x: x.Gparams(0, 1, 2, 1),
            'ABCD':lambda x: x.Aparams(0, 1, 2, 1),
            'invABCD':lambda x: x.Bparams(0, 1, 2, 1),
            'S':lambda x: x.Sparams(0, 1, 2, 1),
            'T':lambda x: x.Tparams(0, 1, 2, 1),
        }

        assert network_rep in rep_actions.keys(), f'`{network_rep}` is not 2Port rep'

        return rep_actions[network_rep](self.schematic)

# In[41]:

T_coil=t_coil()
T_coil.lcapy_self()

# In[42]:

#get this filters abstract transfer function
T_coil.get_tf(with_values=False, ZPK=False)

# In[43]:

#get this filters transfer function
T_coil.get_tf(with_values=True, ZPK=False)

# In[44]:

reset()
#create the nets
net_in=Net('In'); net_out=Net('Out')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

T_coil.SKiDl(net_in, gnd, net_out, gnd)

circ=generate_netlist()
print(circ)

# In[45]:

t_coil_resp=qfilter_explorer(circ, 'Equivalent', start_freq=1e7, stop_freq=1e10)

# In[46]:

t_coil_resp.symbolic_tf(T_coil)

# # Transformers
#
# Am just doing a basic transformer with XSPICE will come back and do the specialty ones at a later time

# ## Basic Transformer with XSPICE¶
#
# With just SPICE the transformer is modeled as a pair of coupled inductors with a coupling constant between them. So this doesn't work well for transformers. Yes, we could do it this way but it would be a laughable facsimile to a real transformer with real cores. And again SPICE is not a field solver. So to increase the capability of SPICE more advanced models have been developed. Unfortunately, most of these SPICE enhancements get swallowed up by proprietary EDA companies. But there is a major extension to Berkely's SPICE (what the core of ngspice is based on) via the Georgia Tech Research Institute that is open source and has been integrated into ngspice; XSPICE.
#
# Here we will just start using XSPICE to create a very simple implementation of a transformer based on what is in the ngspice documentation. But of course, we will do this with SKiDl and wrap it in a class that can give a user or developer a template to bespoke it to their needs.
#

# ## Quick rundown on instantiating XSPICE elements with SKiDl
#
# While XSPICE has a whole part of the ngspice manual dedicated to it (typically Part 2) the basic XSPICE elements are typically documented in Part 1 Chapter 12 "Mixed-Mode and Behavioral Modeling with XSPICE". The important parts of that chapter are "Table 12.1: Port Type Modifiers" and Section 12.5 "Predefined Node Types for event-driven simulation". The rest of the chapter is broken down into three main sections:
#
# -	Analog Models (12.2): these are XSPICE elements with both analog input and outputs like Diodes, S-Domain Transfer Functions, Memristors, etc
#
# -	Hybrid Models (12.3): these are XSPICE elements with a mixture of analog and digital IOs such as DAC, ADC, Controlled Digital Oscillators
#
# -	Digital Models (12.4): there XSPICE elements with full digital IOs such as RTL logic primitives of AND, NAND, NOR, Latches, and Flip-Flops
#
# To create the primitive XSPICE transformer we are going to need two XSPICE *Inductive Coupling* elements (12.2.18) and one XSPICE *Magnetic Core* element (12.2.19)
#
# Let's just do a walkthrough of invoking an *Inductive Coupling* element in SKiDl. First off in the ngspice documentation all XSPICE elements are documented with a three-column table where at the top of the table we get some basic info for the element. What we are after here is the **Spice_Model_Name** row which in this case is `lcouple`. This tells the engine what XSPICE element were using like how `V` tells the engine that the element is a voltage source. The next main section in the table is the Port description *PORT_TABLE* where for each port it has an entry. Notice for an lcouple element it has two ports `l` and `mmf_out` in the center and far left column respectively. For each port, the most important info is **Allowed_Types** were to understand the cryptic type codes we need to refer to "Table 12.1: Port Type Modifiers". For `l` we have two allowed types `h` which is a single-ended input referenced to ground, and `hd` which is a differential input with no reference. Here's the thing, the translation from SKiDl though PySPICE to ngspice will not infer the grounded connection therefore go with the high number of terminals. So in this case port `l` will have two terminals where we are free to make the names of them whatever we want, will go with `io1` and `io2` respectively, but that is your choice though the DOT will go with the first terminal. For the other port `mmf_out` it is only a hd port so we have to create two terminals for it that again can be arbitrarily named but we will go with `mmf1`, `mmf2` here. This is the port that outputs the magnetic flux "current" to another lcouple element or magnetic core element. The last section is the *PARAMETER_TABLE* which can be quite extensive, but for the lcouple element is just a single parameter. Where each parameter will have info about its type, default value, bounds, etc. Where that can, and in our transformer class, used to write the parameter assertions. For an lcouple element, the only parameter is `num_turns` which is the number of turns for that particular lcouple element. Which, we will write the assertion to be that `num_turns` must be an integer greater than or equal to 1. Armed with the info we need to invoke our element lets look at doing so with SKiDl

# The follwing will be a non function cirucit to demnstrate instaint xarray elements with SKiDl before the exstaince class for a full transfomer. We start by create a varibel that holds the number of turns for this inductor and follow it up with an assertion to make sure only valid inputs are allowed

# In[47]:

#var to hold the number of turns
N_turns=5
#assertion
assert isinstance(N_turns, int) and N_turns>=1, 'N_turns must be int >=1'

# We then instantiate the lcouple instance and tie it into the circuit as follows and then dissect what was done

# In[48]:

#refrance lines number in markdown below don't change them
reset()
#create some dummy nets
io1_dnet=Net('io1D'); io2_dnet=Net('io2D'); mmf1_dnet=Net('mmf1D'); mmf2_dnet=Net('mmf1D')

#instatine the XSPICE lcouple elment
l_couple_ex = A( # `A` is the ngspice control leter for XSPICE elements
            io="io1, io2, mmf1, mmf2",  #io1 & io2 \in port`l` and `mmf1` & `mmf2` \in port `mmf_out`
            model=XspiceModel( #`model` is filling in the blank for a `A` element and `XspiceModel` for SkiDl
                "ex_lcouple", #name for this element instance
                "lcouple", #what type of XSPICE element this is
                num_turns=N_turns #element parameters as kwargs
            ),
        )

#tie in the lcouple element to the nets
#first tie to the circuit
l_couple_ex['io1', 'io2']+=io1_dnet, io2_dnet
#second tie for the flux
l_couple_ex['mmf1', 'mmf2']+=mmf1_dnet, mmf2_dnet

circ=generate_netlist()
print(circ)

# In line 7 we instantiate the XSPICE element within a SPICE element type `A`. This is followed up in line 8 with the declaration/creation of the IO terminals. The first port `l` is a `hd` type port therefore differential terminals are appropriately instantiated as `io1` & `io2` followed with port `mmf_out` which is only a `hd` type and so has the arbitrary terminals `mmf1` & `mmf2`. Both these ports are done in one string `"io1, io2, mmf1, mmf2"`. Next in line 9, we invoke `model=XspiceModel` where XspiceModel is the pass-through handler from SKiDl through to the ngspice netlist. The first argument of `XspiceModel` is the name of this element instance in line 10 `ex_lcouple`; where for the full transformer below this will be more descriptive with `primary` and `secondary` to indicate which lcouple element is taking on wich role. The next argument of `XspiceModel` in line 11 must be the **Spice_Model_Name** from the elements document table in the ngspice manual where for a lcouple it's lcouple. The rest of the arguments are pythonic kwags inputs where the argument is the parameter verbatim of its **Parameter_Name:** in the documentation table then set equal to the value we want it to take. In lcouple elements, there is only a single parameter `num_turns` that we set equal to our variable `N_turns` in line 12. From there we have a now standard SkiDl element that we attach to the rest of the circuit like normal. Thou what actually attaches to what is dictated by the port type and what it's meant to do. For a lcouple element port `mmf_out` which is made of terminals `mmf1` & `mmf2` is intended to pass a representation of the magnetic flux to either another lcouples `mmf_out` port or to a magnetic core element.

# ## XSPICE made primitive Transformer
#
# Now know how to translate an XSPICE element from the ngspice docs to a SKiDl element. We now construct a class to act as a very primitive transformer model. And by primitive we mean it does not take into account the resistive and capacitive nature of each of the ports that must be modeled for a more sophisticated transformer model. All the values and basic configuration are taken from the ngspice 32 manual section 12.2.19 examples
#

# In[49]:

#%%writefile -a AC_2_Codes.py
#chapteer 2 section 3 basic_xspice_transformer class
#class with skidl subcircuit to create primitive XSPICE transformer-based
#on the ex in ngspice 32 manual section 12.2.19

class basic_xspice_transformer():
    """
    Basic XSPICE based two-port transformer based on the example
    in ngspice 32 manual section 12.2.19
    """

    def __init__(self, subcirc_ref=None, prim_turns=1, sec_turns=1, parallel=True):
        """
        Create a primitive two-port transformer with a magnetic core
        where core parameters are set in `self.core_specs`

       Args:
            subcirc_ref (str): reference to use for the base of the internal elements
            prim_turns (int; 1; None): the number of turns on the primary port
                of the transformer must be an int >=1

            sec_turns (int; 1; None): the number of turns on the primary port
                of the transformer must be an int >=1

            parallel (bool; True): If True will put the primary and secondary ports
                in parallel dot configuration else will put them in anti- parallel config

        Returns:
            None
        TODO:
            -really investigate the anti parrel config to make sure it's working
                as it's supposed to

        """
        self.subcirc_ref=subcirc_ref

        assert prim_turns>=1 and sec_turns>=1, 'turns must be >= 1'
        self.prim_turns=prim_turns; self.sec_turns=sec_turns

        self.parallel=parallel

        self.core_specs()

    def core_specs(self,
                   H_array=None, B_array=None,
                   area=0.01, length=0.01,
                   input_domain=0.01,
                   fraction=True,
                   mode=1,
                   in_low=-7.0, in_high=7.0,
                   hyst=2.3, out_lower_limit=-2.5e-4,
                   out_upper_limit=2.5e-4,
                  #tried to make it look like the table in the ngspice manual
                  make_plot=False):
        """
        Access method to set all the core properties, if `make_plot` is true
        will produce a plot of the BH curve

        Args:
        (see assertions in code below for each inputs constraints)
        (default values are taken from the two examples in ngspice 32 12.2.19,
        not from the default values in `core`'s docs )

            H_array (np.array/None; None; ATurns/m): array for the core's main BH curve H values
             if None will use default in the ngspice 32 manual examples in 12.2.19;
             used only in pwl mode

            B_array (np.array/None; None; T):array for the core's main BH curve B values
             if None will use default in the ngspice 32 manual examples in 12.2.19;
             used only in pwl mode

            area (float; 0.01; m^2): the cross-section area of the core such that the flux
                will be \psi=B *A where A is the area and B is the B part of the BH curve;
                used only in pwl mode

            length (float; 0.01; m): length of the core will scall the io mmf such that
            H=mmf/L where H is the H part of the BH curve, mmf is the IO mmf and L is the length;
            used only in pwl mode

            input_domain (float; 0.01; ?): docs are not clear what this is

            fraction (bool; True): if True apply smoothing to the BH curve vs nearest value in BH curve array

            mode (int; 1): if `1` use a pwl model of the core else `2` use a hysteresis model

            in_low (float; -7.0; ?): input low value, ?; used only when using hysteresis mode

            in_high (float; 7.0; ?): input high value, ?; used only when using hysteresis mode

            hyst (float; 2.3; ?): hysteresis, ?; used only when using hysteresis mode

            out_lower_limit (float; -2.5e-4; ?): ouput lower limit. ? ; used only when using hysteresis

            out_upper_limit (float; 2.5e-4; ?): ouput upper limit, ? ; used only when using hysteresis

            make_plot (bool; False): if True create a plot of the BH curve and
                returns that plots axis

        Returns:
            create atriputes of XSPICE core parmaters like `self.*_V` that
            hold the parmter value to pass to the core instaince in `self.SKiDl`.
            If `make_plot` is True will create plot of BH curve and retrun it's axis

        TODO:
            - beef up the docs about the hysteresis from a source other then the ngspice manual
            - beef up the plots

        """

        if (H_array==None) or (B_array==None):
            assert (H_array is None) and (B_array is None), 'Both H & B must be None or equal np.arrays'
            H_array=np.array([-1000, -500, -375, -250, -188, -125, -63, 0, 63, 125, 188, 250, 375, 500, 1000])
            B_array=np.array([-3.13e-3, -2.63e-3, -2.33e-3, -1.93e-3, -1.5e-3, -6.25e-4, -2.5e-4, 0, 2.5e-4, 6.25e-4, 1.5e-3, 1.93e-3, 2.33e-3, 2.63e-3, 3.13e-3])
        else:
            assert isinstance(H_array, (np.ndarray, np.generic) ) and isinstance(H_array, (np.ndarray, np.generic) ), 'Both H & B must be None or eual np.arrays'

        assert (H_array.shape==B_array.shape) and (H_array.shape[0]>1) and (H_array.ndim==1), 'H and B arrays must be eual 1 dim arrays'
        self.H_array_V=H_array; self.B_array_V=B_array

        assert isinstance(area, float) and (area>=0), 'area must be a float >=0'
        self.area_V=area

        assert isinstance(length, float) and (length>=0), 'length must be a float >=0'
        self.length_V=length

        assert isinstance(input_domain, float) and (input_domain>=1e-12) and (input_domain<=0.5), 'input_domain must be a float >=1e-12 and <=0.5'
        self.input_domain_V=input_domain

        assert isinstance(fraction, bool), 'fraction must be a bool'
        self.fraction_V=fraction

        assert mode in [1,2], 'mode is a control value 1:pwl, 2:hyst; only'
        self.mode_V=mode

        assert isinstance(in_low, float) and (in_low<in_high), 'in_low must be a float less then in_high'
        self.in_low_V=in_low

        assert isinstance(in_high, float) and (in_low<in_high), 'in_high must be a float greater then in_low'
        self.in_high_V=in_high

        assert isinstance(hyst, float) and (hyst>0.0), 'hyst must be a float greater than 0'
        self.hyst_V=hyst

        assert isinstance(out_lower_limit, float) and (out_lower_limit<out_upper_limit), 'out_lower_limit must be a float less then in_high'
        self.out_lower_limit_V=out_lower_limit

        assert isinstance(out_upper_limit, float) and (out_lower_limit<out_upper_limit), 'out_upper_limit must be a float less then out_lower_limit'
        self.out_upper_limit_V=out_upper_limit

        if make_plot:
            fig, ax=plt.subplots(ncols=1, nrows=1)

            ax.plot(self.H_array_V, self.B_array_V, marker='o' , label='Main BH Curve')
            ax.set_xlabel('magfield_H_[ATurns/m]'); ax.set_ylabel('flux_density_B_[T]')
            ax.axvline(color='black', linestyle='--', alpha=0.5); ax.axhline(color='black', linestyle='--', alpha=0.5)
            ax.grid()
            ax.legend()
            if self.subcirc_ref==None:
                title=''
            else:
                title=self.subcirc_ref
            ax.set_title(f'B-H Curve for Transfomer {title}')

            return ax
    @subcircuit
    def SKiDl(self, term_0, term_1, term_2, term_3, return_elements=False):
        """
        Terminals:
        term_0, term_1, term_2, term_3

        Terminals are defined via:
        ```
        Left_(prime)Termanals - Transformer - Right_(sec)Termanals
                    (dot)   +--core--+  (dot if parallel)
        Postive V_i   term_0-|0 || 2|-term_2     Postive V_o
        Negtive V_i   term_1-|1 || 3|-term_3     Negtive V_o
                             +------+ (dot if anti-parallel)
        ```

        Args:
            return_internls (bool; False): If True return out the internal Voltage Source,
                and Resistance objects in this package
        Returns:
            Returns elements to circuit RC lowpass filter part element object and if `return_internls`
            is True will return the internal voltage and resistance objects in that order
        """

        # Creating an XSPICE part using the SPICE abbreviation 'A'.
        #Createing the primary side
        prim = A(
            io="io1, io2, mmf1, mmf2",  #io1 & io2 \in port`l` and `mmf1` & `mmf2` \in port `mmf_out`
            model=XspiceModel(
                f"prim_{self.subcirc_ref}",
                "lcouple",
                num_turns=self.prim_turns
            ),
        )

        #creating the secondary side
        sec = A(
            io="io1, io2, mmf1, mmf2",
            model=XspiceModel(
                f"sec_{self.subcirc_ref}",
                "lcouple",
                num_turns=self.sec_turns
            ),
        )

        #connect the "inductors" to the outside world
        #the lcouples did not like having their ports named just 1, 2
        prim['io1', 'io2']+=term_0, term_1
        if self.parallel:
            sec['io1', 'io2']+=term_2, term_3
        else:
            sec['io2', 'io1']+=term_2, term_3

        #instatnint the core
        core=A(
            io="top, bot", #couldnt think of a better i/o name
            model=XspiceModel(
                f'core_{self.subcirc_ref}',
                'core',
                H_array=self.H_array_V.tolist(),
                B_array=self.H_array_V.tolist(),
                area=self.area_V,
                length=self.length_V,
                input_domain=self.input_domain_V,
                fraction=self.fraction_V,
                mode=self.mode_V,
                in_low=self.in_low_V,
                in_high=self.in_high_V,
                hyst=self.hyst_V,
                out_lower_limit=self.out_lower_limit_V,
                out_upper_limit=self.out_upper_limit_V

            )
        )

        #connect the core to the mmf control i/o for the lcoupled inductors
        #direct connect of the `mmfs`'s together yield singular matrix, gnd should be fine
        prim['mmf1', 'mmf2']+=core['top'], gnd
        if self.parallel:
            sec['mmf1', 'mmf2']+=core['bot'], gnd
        else:
            sec['mmf2', 'mmf1']+=core['bot'], gnd

    def lcapy_self(self):
        """
        leaving off all the lcapy stuff since it won't do anti parral config
        """
        pass

# ## Parallel Primary and Seconday configeration

# In[50]:

transformer=basic_xspice_transformer(prim_turns=2, sec_turns=5)
#dont change any core prop, but make the plot
transformer.core_specs(make_plot=True);

# In[51]:

reset()
#create the nets
net_in=Net('In'); net_out=Net('Out')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

#conenct the subcircuit
transformer.SKiDl(net_in, gnd, net_out, gnd)

#create a load
rload=R(ref='load', value=100@u_Ohm)
load_amm=V(ref='amm_load', dc_value=0)
net_in & rload & load_amm & gnd

circ=generate_netlist(libs="SpiceLib")
print(circ)

# In[52]:

trans_sim=ac_ease(circ)
trans_sim.ac_sweep_setup(10@u_Hz, 1@u_GHz, 20, 'decade', True)
trans_sim.do_ac_sim()
trans_sim.ac_resultsNB_DF.columns

# In[53]:

par_data=ac_representation_tool(trans_sim.ac_resultsNB_DF)
par_data.make_mag_phase()

# In[54]:

par_data.ac_sim_mag_DF.columns

# In[55]:

par_plot=eecomplex_plot_templets()
fig, [axmag, axphase]=plt.subplots(ncols=1, nrows=2)
par_plot.bode_plot_two_templet(par_data.ac_sim_mag_DF.index,
                               par_data.ac_sim_mag_DF['In_[V][dB]'], par_data.ac_sim_phase_DF['In_[V][deg]'],
                              axs=[axmag, axphase], title='Par Currents')

axmag.semilogx(par_data.ac_sim_mag_DF.index, par_data.ac_sim_mag_DF['Out_[V][dB]'], label='Out')
axphase.semilogx(par_data.ac_sim_mag_DF.index, par_data.ac_sim_phase_DF['Out_[V][deg]'], label='Out')

axmag.legend(); axphase.legend()
plt.tight_layout()

# In[56]:

par_plot=eecomplex_plot_templets()
fig, [axmag, axphase]=plt.subplots(ncols=1, nrows=2)
par_plot.bode_plot_two_templet(par_data.ac_sim_mag_DF.index,
                               par_data.ac_sim_mag_DF['V1_[A][dB]'], par_data.ac_sim_phase_DF['V1_[A][deg]'],
                              axs=[axmag, axphase], title='Par Currents')

axmag.semilogx(par_data.ac_sim_mag_DF.index, par_data.ac_sim_mag_DF['Vamm_load_[A][dB]'], label='Out')
axphase.semilogx(par_data.ac_sim_mag_DF.index, par_data.ac_sim_phase_DF['Vamm_load_[A][deg]'], label='Out')

axmag.legend(); axphase.legend()
plt.tight_layout()

# ## Anti-Parallel Primary and Secondary configuration
#
# Need to verify this better with a transient simulation.

# In[57]:

transformer=basic_xspice_transformer(prim_turns=2, sec_turns=5, parallel=False)

# In[58]:

reset()
#create the nets
net_in=Net('In'); net_out=Net('Out')

#create a 1V AC test source and dummy R and attache to nets
vs=SINEV(ac_magnitude=1@u_V)
rdummy=R(ref='dummy', value=0@u_Ohm)
vs['p', 'n']+=rdummy[1], gnd
rdummy[2]+=net_in

#conenct the subcircuit
transformer.SKiDl(net_in, gnd, net_out, gnd)

#create a load
rload=R(ref='load', value=100@u_Ohm)
load_amm=V(ref='amm_load', dc_value=0)
net_in & rload & load_amm & gnd

circ=generate_netlist(libs="SpiceLib")
print(circ)

# In[59]:

trans_sim=ac_ease(circ)
trans_sim.ac_sweep_setup(10@u_Hz, 1@u_GHz, 20, 'decade', True)
trans_sim.do_ac_sim()
trans_sim.ac_resultsNB_DF.columns

# In[60]:

antipar_data=ac_representation_tool(trans_sim.ac_resultsNB_DF)
antipar_data.make_mag_phase()

# In[61]:

antipar_data.ac_sim_mag_DF.columns

# In[62]:

par_plot=eecomplex_plot_templets()
fig, [axmag, axphase]=plt.subplots(ncols=1, nrows=2)
par_plot.bode_plot_two_templet(antipar_data.ac_sim_mag_DF.index,
                               antipar_data.ac_sim_mag_DF['In_[V][dB]'], antipar_data.ac_sim_phase_DF['In_[V][deg]'],
                              axs=[axmag, axphase], title='Anti-Par Voltages')

axmag.semilogx(antipar_data.ac_sim_mag_DF.index, antipar_data.ac_sim_mag_DF['Out_[V][dB]'], label='Out')
axphase.semilogx(antipar_data.ac_sim_mag_DF.index, antipar_data.ac_sim_phase_DF['Out_[V][deg]'], label='Out')

axmag.legend(); axphase.legend()
plt.tight_layout()

# In[63]:

par_plot=eecomplex_plot_templets()
fig, [axmag, axphase]=plt.subplots(ncols=1, nrows=2)
par_plot.bode_plot_two_templet(antipar_data.ac_sim_mag_DF.index,
                               antipar_data.ac_sim_mag_DF['V1_[A][dB]'], antipar_data.ac_sim_phase_DF['V1_[A][deg]'],
                              axs=[axmag, axphase], title='Anti-Par Currents')

axmag.semilogx(antipar_data.ac_sim_mag_DF.index, antipar_data.ac_sim_mag_DF['Vamm_load_[A][dB]'], label='Out')
axphase.semilogx(antipar_data.ac_sim_mag_DF.index, antipar_data.ac_sim_phase_DF['Vamm_load_[A][deg]'], label='Out')

axmag.legend(); axphase.legend()
plt.tight_layout()

# ## Autotransformer

# ## Isolation

# https://www.allaboutcircuits.com/technical-articles/transformer-isolation/

# ### Test Bench Type

# ### Medical Type

# ## Multi Winding

# ## Cinter-tapped

# ## Current Transformer

# ## Y-Delta Converters

# # Citations

# [1] ALL ABOUT ELECTRONICS. "Self Inductance and Mutual Inductance Explained," YouTube, Feb 12, 2017. [Video file]. Available: https://youtu.be/hoTInTKij0o. [Accessed: Jul 18, 2021].
#
# [2] ALL ABOUT ELECTRONICS. "Dot Convention in Magnetically Coupled Circuits," YouTube, Feb 18, 2017. [Video file]. Available: https://youtu.be/sILgO4sQmRs. [Accessed: Jul 18, 2021].
#
# [3] ALL ABOUT ELECTRONICS. "Mutually Coupled Inductors in Series (Derivation / Proof and Examples)," YouTube, Apr 21, 2017. [Video file]. Available: https://youtu.be/49OL_3L7BFA. [Accessed: Jul 18, 2021].
#
# [4] ALL ABOUT ELECTRONICS. "Mutually Coupled Inductors in Parallel (Derivation / Proof and Examples)," YouTube, Apr 24, 2017. [Video file]. Available: https://youtu.be/NacczEJj_iI. [Accessed: Jul 18, 2021].
#
# [5] B. Razavi, "The Bridged T-Coil [A Circuit for All Seasons]", IEEE Solid-State Circuits Magazine, vol. 7, no. 4, pp. 9-13, 2015. Available: 10.1109/mssc.2015.2474258 [Accessed 27 January 2021] or https://www.seas.ucla.edu/brweb/papers/Journals/BRFall15TCoil.pdf [Accessed: Jul 18, 2021].
#
# [6] Jordan Edmunds. "Bridged T Coil Analysis Part 1 ," YouTube, Feb 23, 2018. [Video file]. Available: https://youtu.be/-kSSMOAhlwU. [Accessed: Jul 18, 2021].
#
# [7] Jordan Edmunds. "Bridged T Coil Analysis Part 2 ," YouTube, Feb 23, 2018. [Video file]. Available: https://youtu.be/vr2oYehKC8Q. [Accessed: Jul 18, 2021].
#
# [8] Vogt, M. Hendrix and P. Nenzi, Ngspice User’s Manual Version 32 (Describes ngspice release version), 32nd ed. 2020, Chapter 12: Mixed-Mode and Behavioral Modeling with XSPICE.
#


![Callysto.ca Banner](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-top.jpg?raw=true)

import matplotlib.pyplot as plt
import plotly as py
import plotly.graph_objs as go
import numpy as np
import math
import ipywidgets as widgets
from IPython.display import display, Math, Latex, HTML, IFrame
from astropy.table import Table, Column
from ipywidgets import interact, interactive

py.offline.init_notebook_mode(connected=True)
%matplotlib inline

font = {'family' : 'sans-serif',
        'weight' : 'normal',
        'size'   : 14}

plt.rc('font', **font)

'''Above, we are importing all the necessary modules in order to run the notebook.
Numpy allows us to define arrays of values for our variables to plot them
matplotlib is what we use to create the figures
the display and widgets are to make the notebook look neat
'''

HTML('''
<form action="javascript:code_toggle()"><input type="submit" id="toggleButton" value="Show Code"></form>''')

***
# Light and Optics
***

Gif taken from https://giphy.com/gifs/fandor-sun-eclipse-3o7OsM9vKFH2ESl0KA/links, August 1st, 2018.
 Figure 1: For hundreds of years, scientists have tried to understand the nature of light. With advances in technology, and inventions like telescopes, we have been able to see farther than ever before.

***

## Introduction

Throughout most of history, humans did not understand light as we do today. As science and technology have progressed over time, so too has our knowledge of the nature of light.

In this lesson, when we say the word "light", we will be referring to visible light (light that comes from the sun, lightbulbs etc.). We will go over how a few key experiments kickstarted a new way of thinking, and a few of the ways that we are able to manipulate light. We will also talk about how our eyes enable us to see.

## Background

If you had to describe to someone what light is, you may have a hard time. Some people think of light as the absence of darkness, but even that doesn't say much about light itself.

Our understanding of light truly began around the 17th century, when a few individuals started to realize that light was not a mystical substance. Scientists (or "natural philosophers", as they were called during that time) recognized that certain properties of light were measurable, and that some properties could be manipulated. Sir Isaac Newton and Ole Rømer were among the first scientists to take a step in this direction.

> ### Isaac Newton's Prism Experiment

Sir Isaac Newton has made contributions to many fields of science and mathematics. In 1666, while spending time at his childhood home in Lincolnshire, England, Newton began experimenting with light.

Using a small slit in his window shutters, Newton passed a narrow beam of sunlight through a glass prism. The light travelled through the prism, and projected a rainbow of color on the other side!

Picture taken from http://media.web.britannica.com/eb-media/10/7710-050-36C066AC.jpg.
 Figure 2: This picture shows how a prism can create a spectrum of color. This is what Newton would have seen in 1666.

Later on, scientists determined that the prism was actually splitting light into its component parts. This phenomenon is called **dispersion**.

Through this experiment, Newton demonstrated that white light was actually made up of all the individual colors of the rainbow!

> ### Ole Rømer and the Speed of Light

For many years, people thought that if somebody lit a match, the light from that match would be instantly visible to everyone, no matter how far away they were. However, in 1676 Ole Rømer proved that this is not the case.

Rømer spent a long time studying the orbit of Io, one of Jupiter's moons. As part of his study, he began predicting the times when Io should be hidden behind Jupiter's shadow (these periods are called eclipses). However, Rømer saw that his predictions for when these eclipses should occur were not always accurate.

Gif taken from https://giphy.com/gifs/timelapse-DXIa1beDspYRy, August 1st, 2018.
 Figure 3: Here we can see Jupiter as it looks through a telescope. You might be able to see a black spot move from the left to the right across Jupiter's surface. This is actually one of Jupiter's many moons!

Rømer then realized that these errors may be because the distance between Io and the Earth was always changing. Rømer thought that when the distance between Io and the Earth increased, it might take a longer time for light coming from Io to reach Earth. If this were the case, then the light must be travelling at a finite speed!

After taking many measurements and using some clever mathematics, Rømer calculated the speed of light to be roughly 220,000,000 m/s, or 792,000,000 km/h.

Today, we have measured the speed of light to be 299,792,458 m/s. Although he was not exactly right, Rømer provided one of the first mathematical calculations for the speed of light.

***
Since the time of Rømer and Newton, scientists have made many new discoveries about the nature of light. While not all of these discoveries agree with one another, here are two things we know for sure:
- Light is made up of a spectrum of color
- Light travels at a speed of 299,792,458 m/s

Now let's talk about some of the ways we can manipulate light.
***

## Reflection

We are all familiar with reflection; chances are, you look at your reflection more than once a day. But have you ever stopped to wonder what is really going on?

Reflection is the term used to describe how light can change direction when it comes into contact with certain surfaces.

When incoming light rays encounter a reflective surface, they bounce off the surface and continue moving in a new direction. The new direction in which it moves is determined by the **law of reflection**.

\begin{equation}
\rm Law\: of\: Reflection: Angle\: of\: Incidence = Angle\: of\: Reflection
\end{equation}

On the animation below, click on the flashlight to turn it on, and move your mouse to change the angle of incidence.

#IFrame('Animations/reflect.html',width=500,height=320)

%%html

As seen above, the **normal** is what we call the line that forms a 90$^{\circ}$ angle with  the surface. The **angle of incidence** is what we call the angle between the flash lights beam and the normal. Similarly, the **angle of reflection** is the angle that the newly reflected light beam makes with the normal. The law of reflection states that these two angles will always be equal.

## Refraction

Have you ever tried to reach down and grab an object sitting at the bottom of a pool of water? If you have, you may have noticed that the object isn't actually in the location that you thought it was.

 Image taken from http://legacy.sciencelearn.org.nz/Contexts/Light-and-Sight/Sci-Media/Video/Refraction/(quality)/hi on August 3rd, 2018.
 Figure 4: When you are looking into a body of water from above, the objects you see beneath the surface are not actually where they appear to be.

This phenomenon occurs because the light travelling to your eyes from the bottom of the pool **refracts**, or changes its direction of travel, when it transitions from water to air.

The **index of refraction** is a value that we use to show how much light will bend when travelling through a substance. For example, the index of refraction for air is approximately 1.00, and the index of refraction for water is about 1.33. Because these indexes are different, light will bend when passing from water to air, or vice versa.

Use the animation below to see how light refracts when passing from air to water. Click on the flashlight to turn it on.

#IFrame('Animations/refract.html',width=520,height=320)

%%html

Mathematically, reflection can be described using the following equation, known as Snell's Law:

\begin{equation}
\textrm{Snells Law:}\: n_1\sin(\theta_1) = n_2\sin(\theta_2)
\end{equation}

where $n_1$ is the index of refraction for the first medium, $\theta_1$ is the incident angle, $n_2$ is the index of refraction for the second medium, and $\theta_2$ is the angle of refraction.

Light will bend *towards* the normal when travelling from a medium with a *lower* index of refraction to one with a *higher* index of refraction, and vice versa.

***
Some of the most beautiful sights in nature are caused by reflection and refraction. Here are a couple of examples:

### Rainbows

Rainbows are a result of both reflection and refraction. As its raining, each water droplet acts like a tiny prism, just like the one we saw in Figure 2. The water droplets split visible light into colors, and these colors are then reflected back towards our eyes.

 Image taken from https://waterstories.nestle-waters.com/environment/how-does-a-rainbow-form/ on August 3rd, 2018.
 Figure 5: Water droplets use reflection and refraction to create the beautiful rainbows that we see while it is raining.

### Mirages

Have you ever been driving on a sunny day, and up ahead it looks as though a stream of water is running across the road? You are really seeing a mirage.
Mirages also occur because of refraction, but they do not result in a display of color like a rainbow. This type of refraction occurs due to a difference in temperature between separate layers of air.

As we were describing before, refraction occurs when light travels from one substance to another. Well, it turns out that hot air and cold air are actually different enough to act as different substances. Therefore, light will refract when passing through one to the other.

 Image taken from https://edexcellence.net/articles/what-the-mirage-gets-wrong-on-teacher-development on August 3rd, 2018.
 Figure 6: Although it may look like water running across the road, it is actually a mirage. These commonly occur in desert areas, where the road can become very hot.

When you are looking at a mirage, it can look as though the air is wavy and fluid, which is why it is common to think that you are looking at water. This appearance occurs when layers of hot and cold air are mixing together, and light passing through these layers is constantly being refracted in different directions.

You may see a mirage appear on top of a hot roadway, behind the exhaust pipe of a plane or car, or around any other source of heat.

## Applications of Reflection and Refraction

### Lenses

If you have glasses, or contact lenses, then you are constantly using refraction in order to help you see! Lenses use refraction to point light in specific directions.

Generally speaking, there are two types of lenses: **convex** and **concave**.

To see how each type of lens affects light, use the following animation.

#IFrame('Animations/convex.html',width=520,height=420)

%%html

As seen above, a convex lens focuses light towards a specific point, while a concave lens will spread light away from a point. These lenses can be combined in many ways in order to produce different effects. For example, a camera lens uses a series of both convex and concave lenses in order to direct incoming light towards the back of the camera.

 Image taken from https://www.reddit.com/r/pic/comments/3o3b7w/camera_lens_cut_in_half/ on August 3rd, 2018.
 Figure 5: This is what the inside of a camera lens looks like. The photographer can adjust how they want the picture to look by changing the distance between the individual lenses.

## Vision

Our eyes are very complex organs, but the process that enables us to see is actually pretty simple. The basic steps are as follows:

1. Light enters the eye through the **pupil**
2. The convex **lens** behind the pupil directs incoming light towards the **retina**, which is like a screen at the back of our eye.
3. The retina then sends this image to the brain.
4. The brain then interprets the image.

 Image taken from https://openclipart.org/detail/261647/eye-diagram on August 3rd, 2018.
 Figure 6: This diagram shows some of the key components of the eye that enable us to see.

However, the image that is projected onto the retina is actually upside down!

 Image taken from https://www.eetimes.com/author.asp?section_id=14&doc_id=1282795 on August 3rd, 2018.
 Figure 7: The convex lens at the front of our eye actually flips images upside down.

So the retina actually sends an upside down image to the brain, and the brain automatically flips the image rightside up.

Use the following link to see an animation showing how a convex lens flips images upside down:https://phet.colorado.edu/sims/geometric-optics/geometric-optics_en.html.

## Technology & Inventions

### The Telescope

The first telescope was made by Hans Lippershey in 1608, but it was Galileo Galilee who became famous by using it for astronomy. There are many different types of telescopes, but they all use reflection and refraction to make far away objects appear closer.

A telescope uses a large opening to collect incoming light, and then directs this light towards your eye by using mirrors and lenses.

 Image taken from https://www.skyandtelescope.com/press-releases/tips-for-first-time-telescope-buyers/ on August 3rd, 2018.
 Figure 8: Telescopes come in many different shapes and sizes.

The reason why things look bigger when looking through a telescope is because of the lenses.

### The Microscope

- talk about invention of microscope
- why it works

## Conclusion

Our understanding of light is the result of hundreds of years of research and innovation. Along the way, we have created incredible new technologies that have allowed us to look further than ever before.

 Image taken from https://xenlife.com.au/hubble-space-telescope-important/ on August 3rd, 2018.
 Figure 9: The Hubble Space Telescope has shown us pictures of galaxies that are billions of light years away.

[![Callysto.ca License](https://github.com/callysto/curriculum-notebooks/blob/master/callysto-notebook-banner-bottom.jpg?raw=true)](https://github.com/callysto/curriculum-notebooks/blob/master/LICENSE.md)


import pandas as pd
import numpy as np
from six import string_types

# From Penguoin Utils.py

def _export_table(table, fname):
    """Export DataFrame to .csv"""
    import os.path as op
    extension = op.splitext(fname.lower())[1]
    if extension == '':
        fname = fname + '.csv'
    table.to_csv(fname, index=None, sep=',', encoding='utf-8',
                 float_format='%.4f', decimal='.')

def reshape_data(df, id, dv='DV', rm='Time'):
    """Reshape data from human-readable to analysis shape

    See: https://deparkes.co.uk/2016/10/28/reshape-pandas-data-with-melt/

    Parameters
    ----------
    df : DataFrame
        Dataframe in original shape
    id : string
        Column to use as identifier variables. (e.g. "Subjects")
    dv : string
        Name of the dependant variables (e.g. "DV" or "Scores")
    rm : string
        Name of the measurements (e.g. "Time" or "Weekday")

    Returns
    -------
    df_reshaped : DataFrame
        Reshaped DataFrame

    Examples
    --------

    Human-readable:

    *Values represent scores at a cognitive test at different times of the day.
    Ss = subject*::

        Ss    10am  2pm   6pm
        1     3.5   3.2   2.8
        2     2.1   2.4   2.8

    Pingouin::

        Ss    Score    Time
        1     3.5      10am
        1     3.2      2pm
        1     2.8      6pm
        2     2.1      10am
        2     2.4      2pm
        2     2.8      6pm

    >>> import pandas as pd
    >>> from pingouins import reshape_data
    >>> data = {'Ss': [1, 2, 3],
    >>>        '10am': [12, 6, 5],
    >>>        '2pm': [10, 6, 11],
    >>>        '6pm': [8, 5, 7]}
    >>> df = pd.DataFrame(data, columns=['Ss', '10am', '2pm', '6pm'])
    >>> reshaped = reshape_data(df, 'Ss', dv="Score", rm="Time")
    >>> print(reshaped)
        Ss  Time  Score
        1   10am  12
        1   2pm   10
        1   6pm   8
        2   10am  6
        2   2pm   6
        2   6pm   5
        3  10am   5
        3   2pm   11
        3   6pm   7
    """
    return pd.melt(df, id_vars=id, var_name=rm, value_name=dv).sort_values(
        by=id)

def _remove_na(x, y, paired=False):
    """Remove missing values in paired and independant measurements.
    """
    x_na = np.any(np.isnan(x))
    y_na = np.any(np.isnan(y))
    if (x_na or y_na) and paired:
        ar = np.c_[x, y]
        ar = ar[~np.isnan(ar).any(axis=1)]
        x, y = ar[:, 0], ar[:, 1]
    elif (x_na or y_na) and not paired:
        x = np.array(list(filter(lambda v: v == v, x))) if x_na else x
        y = np.array(list(filter(lambda v: v == v, y))) if y_na else y
    return x, y

def _remove_rm_na(dv=None, within=None, data=None):
    """Remove subject(s) with one or more missing values in repeated
    measurements.
    """
    rm = list(data[within].dropna().unique())
    n_rm = len(rm)
    n_obs = int(data.groupby(within)[dv].count().max())
    data['ID_Subj'] = np.tile(np.arange(n_obs), n_rm)

    # Efficiently remove subjects with one or more missing values
    data = data.set_index('ID_Subj')

    # Find index with nan
    iloc_nan = pd.isnull(data).any(1).nonzero()[0]
    idx_nan = data.index[iloc_nan].values
    print('\nNote: %i subject(s) removed because of missing value(s).\n'
          % len(idx_nan))
    return data.drop(idx_nan).reset_index(drop=True)

def _check_eftype(eftype):
    """Check validity of eftype"""
    if eftype.lower() in ['none', 'hedges', 'cohen', 'glass', 'r',
                          'eta-square', 'odds-ratio', 'auc']:
        return True
    else:
        return False

def _check_dataframe(dv=None, between=None, within=None, effects=None,
                     data=None):
    """Check dataframe"""
    # Check input arguments
    if not isinstance(data, pd.DataFrame):
        raise ValueError('Data must be a pandas dataframe')
    if any(v is None for v in [dv, data]):
        raise ValueError('DV and data must be specified')
    if effects not in ['within', 'between', 'interaction', 'all']:
        raise ValueError('Effects must be: within, between, interaction, all')
    if effects == 'within' and not isinstance(within, string_types):
        raise ValueError('within must be specified when effects=within')
    elif effects == 'between' and not isinstance(between, string_types):
        raise ValueError('between must be specified when effects=between')
    elif effects == 'interaction':
        for input in [within, between]:
            if not isinstance(input, string_types):
                raise ValueError('within and between must be specified when \
                effects=interaction')

def _extract_effects(dv=None, between=None, within=None, effects=None,
                     data=None):
    """Extract main effects"""
    # Check the dataframe
    _check_dataframe(dv=dv, between=between, within=within, effects=effects,
                     data=data)

    datadic = {}
    nobs = np.array([], dtype=int)

    # Extract number of pairwise comparisons
    if effects.lower() in ['within', 'between']:
        col = within if effects == 'within' else between
        # Extract data
        labels = list(data[col].unique())
        for l in labels:
            datadic[l] = data[data[col] == l][dv]
            nobs = np.append(nobs, len(datadic[l]))

    elif effects.lower() == 'interaction':
        labels_with = list(data[within].unique())
        labels_betw = list(data[between].unique())
        for lw in labels_with:
            for l in labels_betw:
                tmp = data[data[within] == lw]
                datadic[lw, l] = tmp[tmp[between] == l][dv]
                nobs = np.append(nobs, len(datadic[lw, l]))

    dt_array = pd.DataFrame.from_dict(datadic)
    return dt_array, nobs

def gzscore(x):
    """Geometric standard (Z) score.

    Geometric Z-scores are better measures of dispersion than arithmetic
    z-scores when the sample data come from a log-normally distributed
    population.

    See https://en.wikipedia.org/wiki/Geometric_standard_deviation

    Parameters
    ----------
    x : array_like
        Array of raw values

    Returns
    -------
    gzscore : array_like
        Array of geometric z-scores (same shape as x)

    Examples
    --------
    Standardize a log-normal array

        >>> import numpy as np
        >>> from pingouin import gzscore
        >>> np.random.seed(123)
        >>> raw = np.random.lognormal(size=100)
        >>> print(raw.mean().round(3), raw.std().round(3))
            1.849 2.282
        >>> z = gzscore(raw)
        >>> print(z.mean(), z.std())
            0 0.995
    """
    from scipy.stats import gmean
    # Geometric mean
    geo_mean = gmean(x)
    # Geometric standard deviation
    gstd = np.exp(np.sqrt(np.sum((np.log(x / geo_mean))**2) / (len(x) - 1)))
    # Geometric z-score
    return np.log(x / geo_mean) / np.log(gstd)

def test_normality(*args, alpha=.05):
    """Test the normality of one or more array.

    Parameters
    ----------
    sample1, sample2,... : array_like
        Array of sample data. May be different lengths.

    Returns
    -------
    normal : boolean
        True if x comes from a normal distribution.
    p : float
        P-value.

    See Also
    --------
    test_homoscedasticity : Test equality of variance.
    test_sphericity : Mauchly's test for sphericity.

    Examples
    --------
    1. Test the normality of one array.

        >>> import numpy as np
        >>> from pingouin import test_normality
        >>> np.random.seed(123)
        >>> x = np.random.normal(size=100)
        >>> normal, p = test_normality(x, alpha=.05)
        >>> print(normal, p)
        True 0.27

    2. Test the normality of two arrays.

        >>> import numpy as np
        >>> from pingouin import test_normality
        >>> np.random.seed(123)
        >>> x = np.random.normal(size=100)
        >>> y = np.random.rand(100)
        >>> normal, p = test_normality(x, y, alpha=.05)
        >>> print(normal, p)
        [True   False] [0.27   0.0005]
    """
    from scipy.stats import shapiro
    k = len(args)
    p = np.zeros(k)
    normal = np.zeros(k, 'bool')
    for j in range(k):
        _, p[j] = shapiro(args[j])
        normal[j] = True if p[j] > alpha else False

    if k == 1:
        normal = bool(normal)
        p = float(p)

    return normal, p

def test_homoscedasticity(*args, alpha=.05):
    """Test equality of variance.

    If data are normally distributed, uses Bartlett (1937).
    If data are not-normally distributed, uses Levene (1960).

    Parameters
    ----------
    sample1, sample2,... : array_like
        Array of sample data. May be different lengths.

    Returns
    -------
    equal_var : boolean
        True if data have equal variance.
    p : float
        P-value.

    See Also
    --------
    test_normality : Test the normality of one or more array.
    test_sphericity : Mauchly's test for sphericity.

    Examples
    --------
    Test the homoscedasticity of two arrays.

        >>> import numpy as np
        >>> from pingouin import test_homoscedasticity
        >>> np.random.seed(123)
        >>> # Scale = standard deviation of the distribution.
        >>> x = np.random.normal(loc=0, scale=1., size=100)
        >>> y = np.random.normal(loc=0, scale=0.8,size=100)
        >>> print(np.var(x), np.var(y))
            1.27 0.60
        >>> equal_var, p = test_homoscedasticity(x, y, alpha=.05)
        >>> print(equal_var, p)
            False 0.0002
    """
    from scipy.stats import levene, bartlett
    k = len(args)
    if k < 2:
        raise ValueError("Must enter at least two input sample vectors.")

    # Test normality of data
    normal, _ = test_normality(*args)
    if np.count_nonzero(normal) != normal.size:
        # print('Data are not normally distributed. Using Levene test.')
        _, p = levene(*args)
    else:
        _, p = bartlett(*args)

    equal_var = True if p > alpha else False
    return equal_var, p

def test_dist(*args, dist='norm'):
    """Anderson-Darling test of distribution.

    Parameters
    ----------
    sample1, sample2,... : array_like
        Array of sample data. May be different lengths.
    dist : string
        Distribution ('norm', 'expon', 'logistic', 'gumbel')

    Returns
    -------
    from_dist : boolean
        True if data comes from this distribution.
    """
    from scipy.stats import anderson
    k = len(args)
    from_dist = np.zeros(k, 'bool')
    sig_level = np.zeros(k)
    for j in range(k):
        st, cr, sig = anderson(args[j], dist=dist)
        from_dist[j] = True if (st > cr).any() else False
        sig_level[j] = sig[np.argmin(np.abs(st - cr))]

    if k == 1:
        from_dist = bool(from_dist)
        sig_level = float(sig_level)
    return from_dist, sig_level

def test_sphericity(X, alpha=.05):
    """Mauchly's test for sphericity

    https://www.mathworks.com/help/stats/mauchlys-test-of-sphericity.html

    Warning: results can slightly differ than R or Matlab. If you can,
    always double-check your results.

    Parameters
    ----------
    X : array_like
        Data array of shape (n_observations, n_repetitions)
    alpha : float, optional
        Significance level

    Returns
    -------
    sphericity : boolean
        True if data have the sphericity property.
    W : float
        Mauchly's W statistic
    chi_sq : float
        Chi-square statistic
    ddof : int
        Degrees of freedom
    p : float
        P-value.
    eps : float
        Epsilon adjustment factor.

    See Also
    --------
    test_homoscedasticity : Test equality of variance.
    test_normality : Test the normality of one or more array.

    Examples
    --------
    Test the sphericity of an array with 30 observations *
    3 repeated measures

        >>> import numpy as np
        >>> from pingouin import test_sphericity
        >>> np.random.seed(123)
        >>> x = np.random.normal(loc=0, scale=1., size=30)
        >>> y = np.random.normal(loc=0, scale=0.8,size=30)
        >>> z = np.random.normal(loc=0, scale=0.9,size=30)
        >>> X = np.c_[x, y, z]
        >>> sphericity, W, chi_sq, ddof, p, eps = test_sphericity(X)
        >>> print(sphericity, p)
        True 0.56
        """
    from scipy.stats import chi2
    n = X.shape[0]

    # Compute the covariance matrix
    S = np.cov(X, rowvar=0)
    p = S.shape[1]
    d = p - 1

    # Orthonormal contrast matrix
    C = np.array(np.triu(np.ones((p, d))), order='F')
    C.reshape(-1, order='F')[1::p + 1] = -np.arange(d)
    C, _ = np.linalg.qr(C)
    d = C.shape[1]
    T = C.T.dot(S).dot(C)

    # Compute epsilon
    eig = np.linalg.eigvals(T)
    eps = np.sum(eig) ** 2 / ((S.shape[0] - 1) * np.sum(eig**2))
    # Alternative:
    # eps = np.trace(T) ** 2 / ((S.shape[0] - 1) * np.sum(
    # np.sum(T * T, axis=1)))

    # Mauchly's statistic
    W = np.linalg.det(T) / (np.trace(T) / (p - 1))**d

    # Chi-square statistic
    nr = n - np.linalg.matrix_rank(X)
    dd = 1 - (2 * d**2 + d + 2) / (6 * d * nr)
    chi_sq = -np.log(W) * dd * nr
    ddof = d * (d + 1) / 2 - 1
    pval = chi2.sf(chi_sq, ddof)

    # Second order approximation
    pval2 = chi2.sf(chi_sq, ddof + 4)
    w2 = (d + 2) * (d - 1) * (d - 2) * (2 * d**3 + 6 * d * d + 3 * d + 2) / \
         (288 * d * d * nr * nr * dd * dd)
    pval += w2 * (pval2 - pval)

    sphericity = True if pval > alpha else False
    return sphericity, W, chi_sq, ddof, pval, eps

def ttest(x, y, paired=False, tail='two-sided', correction='auto', r=.707):
    """T-test.

    Parameters
    ----------
    x : array_like
        First set of observations.
    y : array_like or float
        Second set of observations. If y is a single value, a one-sample T-test
        is computed.
    paired : boolean
        Specify whether the two observations are related (i.e. repeated
        measures) or independant.
    tail : string
        Specify whether to return two-sided or one-sided p-value.
    correction : string or boolean
        For unpaired two sample T-tests, specify whether or not to correct for
        unequal variances using Welch separate variances T-test. If 'auto', it
        will automatically uses Welch T-test when the sample sizes are unequal,
        as recommended by Zimmerman 2004.
    r : float
        Cauchy scale factor for computing the Bayes Factor.
        Smaller values of r (e.g. 0.5), may be appropriate when small effect
        sizes are expected a priori; larger values of r are appropriate when
        large effect sizes are expected (Rouder et al 2009).
        The default is 0.707 (= np.sqrt(2) / 2).

    Returns
    -------
    stats : pandas DataFrame
        T-test summary ::

        'T-val' : T-value
        'p-val' : p-value
        'dof' : degrees of freedom
        'cohen-d' : Cohen d effect size
        'power' : achieved power of the test ( = 1 - type II error)
        'BF10' : Bayes Factor of the alternative hypothesis

    Notes
    -----
    Missing values are automatically removed from the data. If x and y are
    paired, the entire row is removed.

    Examples
    --------
    1. One-sample T-test.

        >>> from pingouin import ttest
        >>> x = [5.5, 2.4, 6.8, 9.6, 4.2]
        >>> ttest(x, 4)
            T-val   p-val  dof  cohen-d  power   BF10
            1.397  0.2348    4    0.699  0.226  0.766

    2. Paired two-sample T-test (one-tailed).

        >>> from pingouin import ttest
        >>> pre = [5.5, 2.4, 6.8, 9.6, 4.2]
        >>> post = [6.4, 3.4, 6.4, 11., 4.8]
        >>> ttest(pre, post, paired=True, tail='one-sided')
            T-val   p-val  dof  cohen-d  power   BF10
            -2.308   0.04    4     0.28  0.132  1.864

    3. Paired two-sample T-test with missing values.

        >>> from pingouin import ttest
        >>> from numpy import nan
        >>> pre = [5.5, 2.4, nan, 9.6, 4.2]
        >>> post = [6.4, 3.4, 6.4, 11., 4.8]
        >>> ttest(pre, post, paired=True)
            T-val    p-val  dof  cohen-d  power    BF10
            -5.902  0.0097    3   -0.354  0.074  24.926

    4. Independant two-sample T-test (equal sample size).

        >>> from pingouin import ttest
        >>> import numpy as np
        >>> np.random.seed(123)
        >>> x = np.random.normal(loc=7, size=20)
        >>> y = np.random.normal(loc=4, size=20)
        >>> ttest(x, y, correction='auto')
            T-val     p-val  dof  cohen-d  power   BF10
            9.106  4.30e-11   38     2.88    1.0  1.4e8

    5. Independant two-sample T-test (unequal sample size).

        >>> from pingouin import ttest
        >>> import numpy as np
        >>> np.random.seed(123)
        >>> x = np.random.normal(loc=7, size=20)
        >>> y = np.random.normal(loc=6.5, size=15)
        >>> ttest(x, y, correction='auto')
            T-val     p-val  dof   dof-corr  cohen-d  power   BF10
            2.327     0.027   33      30.75    0.792  0.614  2.454
    """
    from scipy.stats import ttest_rel, ttest_ind, ttest_1samp
    from pingouin import ttest_power, compute_effsize
    x = np.asarray(x)
    y = np.asarray(y)

    if x.size != y.size and paired:
        print('x and y have unequal sizes. Switching to paired == False.')
        paired = False

    # Remove NA
    x, y = _remove_na(x, y, paired=paired)
    nx = x.size
    ny = y.size
    stats = pd.DataFrame({}, index=['T-test'])

    if ny == 1:
        # Case one sample T-test
        tval, pval = ttest_1samp(x, y)
        dof = nx - 1
        pval = pval / 2 if tail == 'one-sided' else pval

    if ny > 1 and paired is True:
        # Case paired two samples T-test
        tval, pval = ttest_rel(x, y)
        dof = nx - 1
        bf = bayesfactor_ttest(tval, nx, ny, paired=True, r=r)

    elif ny > 1 and paired is False:
        dof = nx + ny - 2
        # Case unpaired two samples T-test
        if correction is True or (correction == 'auto' and nx != ny):
            # Use the Welch separate variance T-test
            tval, pval = ttest_ind(x, y, equal_var=False)
            # dof are approximated using Welch–Satterthwaite equation
            vx = x.var(ddof=1)
            vy = y.var(ddof=1)
            dof_corr = (vx / nx + vy / ny)**2 / ((vx / nx)**2 / (nx - 1) +
                                                 (vy / ny)**2 / (ny - 1))
            stats['dof-corr'] = dof_corr
        else:
            tval, pval = ttest_ind(x, y, equal_var=True)

    pval = pval / 2 if tail == 'one-sided' else pval

    # Effect size and achieved power
    d = compute_effsize(x, y, paired=paired, eftype='cohen')
    power = ttest_power(d, nx, ny, paired=paired, tail=tail)

    # Bayes factor
    bf = bayesfactor_ttest(tval, nx, ny, paired=paired, tail=tail, r=r)

    # Fill output DataFrame
    stats['dof'] = dof
    stats['T-val'] = tval.round(3)
    stats['p-val'] = pval
    stats['tail'] = tail
    stats['cohen-d'] = np.abs(d).round(3)
    stats['power'] = power
    stats['BF10'] = bf

    col_order = ['T-val', 'p-val', 'dof', 'dof-corr', 'tail', 'cohen-d',
                 'power', 'BF10']
    stats = stats.reindex(columns=col_order)
    stats.dropna(how='all', axis=1, inplace=True)
    return stats

def rm_anova(dv=None, within=None, data=None, correction='auto',
             remove_na=True, detailed=False, export_filename=None):
    """One-way repeated measures ANOVA.

    Results have been tested against R and JASP.

    Parameters
    ----------
    dv : string
        Name of column containing the dependant variable.
    within : string
        Name of column containing the within factor.
    data : pandas DataFrame
        DataFrame
    correction : string or boolean
        If True, return Greenhouse-Geisser corrected p-value.
        If 'auto' (default), compute Mauchly's test of sphericity to determine
        whether the p-values needs to be corrected.
    remove_na : boolean
        If True, automatically remove from the analysis subjects with one or
        more missing values::

            Ss    x1       x2       x3
            1     5.0      4.2      nan
            2     4.6      3.6      3.9

        In this example, if remove_na == True, Ss 1 will be removed from the
        ANOVA because of the x3 missing value. If False, the two non-missing
        values will be included in the analysis.
    detailed : boolean
        If True, return a full ANOVA table
    export_filename : string
        Filename (without extension) for the output file.
        If None, do not export the table.
        By default, the file will be created in the current python console
        directory. To change that, specify the filename with full path.

    Returns
    -------
    aov : DataFrame
        ANOVA summary ::

        'Source' : Name of the within-group factor
        'ddof1' : Degrees of freedom (numerator)
        'ddof2' : Degrees of freedom (denominator)
        'F' : F-value
        'p-unc' : Uncorrected p-value
        'np2' : Partial eta-square effect size
        'p-GG-corr' : Greenhouse-Geisser corrected p-value
        'W-Mauchly' : Mauchly statistic
        'X2-Mauchly' : Chi-square statistic of the Mauchly test
        'DF-Mauchly' : Degrees of freedom for the Mauchly test
        'p-Mauchly' : p-value of the Mauchly test
        'sphericity' : sphericity of the data (boolean)

    See Also
    --------
    anova : One-way ANOVA
    mixed_anova : Two way mixed ANOVA
    friedman : Non-parametric one-way repeated measures ANOVA

    Notes
    -----
    The effect size reported in Pingouin is the partial eta-square.
    However, one should keep in mind that for one-way repeated-measures ANOVA,
    partial eta-square is the same as eta-square.

    For more details, see Bakeman 2005; Richardson 2011.

    Examples
    --------
    Compute a one-way repeated-measures ANOVA.

        >>> import pandas as pd
        >>> from pingouin import rm_anova, print_table
        >>> df = pd.read_csv('dataset.csv')
        >>> aov = rm_anova(dv='DV', within='Time', data=df, correction='auto',
                           remove_na=True, detailed=True,
                           export_filename='rm_anova.csv')
        >>> print_table(aov)
    """
    from scipy.stats import f
    # Check data
    _check_dataframe(dv=dv, within=within, data=data, effects='within')

    # Remove NaN
    if remove_na and data[dv].isnull().values.any():
        data = _remove_rm_na(dv=dv, within=within, data=data)

    # Reset index (avoid duplicate axis error)
    data = data.reset_index(drop=True)

    # Sort values (to avoid bug when creating 'Subj' column)
    data = data.sort_values(by=within, kind='mergesort')

    # Groupby
    grp_with = data.groupby(within)[dv]
    rm = list(data[within].unique())
    n_rm = len(rm)
    n_obs = int(data.groupby(within)[dv].count().max())
    grandmean = data[dv].mean()

    # Calculate sums of squares
    sstime = ((grp_with.mean() - grandmean)**2 * grp_with.count()).sum()
    sswithin = grp_with.apply(lambda x: (x - x.mean())**2).sum()
    data['Subj'] = np.tile(np.arange(n_obs), n_rm)
    grp_subj = data.groupby('Subj')[dv]
    sssubj = n_rm * np.sum((grp_subj.mean() - grandmean)**2)
    sserror = sswithin - sssubj

    # Calculate degrees of freedom
    ddof1 = n_rm - 1
    ddof2 = ddof1 * (n_obs - 1)

    # Calculate F and p-values
    mserror = sserror / (ddof2 / ddof1)
    fval = sstime / mserror
    p_unc = f(ddof1, ddof2).sf(fval)

    # Calculating partial eta-square
    # Similar to (fval * ddof1) / (fval * ddof1 + ddof2)
    np2 = sstime / (sstime + sserror)

    # Reshape and remove NAN for sphericity estimation and correction
    data_pivot = data.pivot(index='Subj', columns=within, values=dv).dropna()

    # Compute sphericity using Mauchly's test
    # Sphericity assumption only applies if there are more than 2 levels
    if correction == 'auto' or (correction is True and n_rm >= 3):
        sphericity, W_mauchly, chi_sq_mauchly, ddof_mauchly, \
            p_mauchly, eps = test_sphericity(data_pivot.values, alpha=.05)

        if correction == 'auto':
            correction = True if not sphericity else False
    else:
        correction = False

    # If required, apply Greenhouse-Geisser correction for sphericity
    if correction:
        # print('eps = ', eps)
        corr_ddof1, corr_ddof2 = [np.maximum(d * eps, 1.) for d in
                                  (ddof1, ddof2)]
        p_corr = f(corr_ddof1, corr_ddof2).sf(fval)

    # Create output dataframe
    if not detailed:
        aov = pd.DataFrame({'Source': within,
                            'ddof1': ddof1,
                            'ddof2': ddof2,
                            'F': fval,
                            'p-unc': p_unc,
                            'np2': np2
                            }, index=[0])
        if correction:
            aov['p-GG-corr'] = p_corr
            aov['W-Mauchly'] = W_mauchly
            aov['X2-Mauchly'] = chi_sq_mauchly
            aov['DF-Mauchly'] = ddof_mauchly
            aov['p-Mauchly'] = p_mauchly
            aov['sphericity'] = sphericity

        col_order = ['Source', 'ddof1', 'ddof2', 'F', 'p-unc',
                     'p-GG-corr', 'np2', 'sphericity', 'W-Mauchly',
                     'X2-Mauchly', 'DF-Mauchly', 'p-Mauchly']
    else:
        aov = pd.DataFrame({'Source': [within, 'Error'],
                            'SS': [sstime, sserror],
                            'DF': [ddof1, ddof2],
                            'MS': [sstime / ddof1, sserror / ddof2],
                            'F': [fval, np.nan],
                            'p-unc': [p_unc, np.nan],
                            'np2': [np2, np.nan]
                            })
        if correction:
            aov['p-GG-corr'] = [p_corr, np.nan]
            aov['W-Mauchly'] = [W_mauchly, np.nan]
            aov['X2-Mauchly'] = [chi_sq_mauchly, np.nan]
            aov['DF-Mauchly'] = np.array([ddof_mauchly, 0], 'int')
            aov['p-Mauchly'] = [p_mauchly, np.nan]
            aov['sphericity'] = [sphericity, np.nan]

        col_order = ['Source', 'SS', 'DF', 'MS', 'F', 'p-unc', 'p-GG-corr',
                     'np2', 'sphericity', 'W-Mauchly', 'X2-Mauchly',
                     'DF-Mauchly', 'p-Mauchly']

    aov = aov.reindex(columns=col_order)
    aov.dropna(how='all', axis=1, inplace=True)
    # Export to .csv
    if export_filename is not None:
        _export_table(aov, export_filename)
    return aov

def anova(dv=None, between=None, data=None, detailed=False,
          export_filename=None):
    """One-way ANOVA.

    Results have been tested against R and JASP.

    Parameters
    ----------
    dv : string
        Name of column containing the dependant variable.
    between : string
        Name of column containing the between factor.
    data : pandas DataFrame
        DataFrame
    detailed : boolean
        If True, return a detailed ANOVA table
    export_filename : string
        Filename (without extension) for the output file.
        If None, do not export the table.
        By default, the file will be created in the current python console
        directory. To change that, specify the filename with full path.

    Returns
    -------
    aov : DataFrame
        ANOVA summary ::

        'Source' : name of the between-group factor
        'ddof1' : degrees of freedom (numerator)
        'ddof2' : degrees of freedom (denominator)
        'F' : F-value
        'p-unc' : uncorrected p-value
        'np2' : Partial eta-square effect size

    See Also
    --------
    rm_anova : One-way repeated measures ANOVA
    mixed_anova : Two way mixed ANOVA
    kruskal : Non-parametric one-way ANOVA

    Notes
    -----
    The effect size reported in Pingouin is the partial eta-square.
    However, one should keep in mind that for one-way ANOVA
    partial eta-square is the same as eta-square and generalized eta-square.

    For more details, see Bakeman 2005; Richardson 2011.

    Examples
    --------
    Compute a one-way ANOVA.

        >>> import pandas as pd
        >>> from pingouin import anova, print_table
        >>> df = pd.read_csv('dataset.csv')
        >>> aov = anova(dv='DV', between='Group', data=df,
                        detailed=True, export_filename='anova.csv')
        >>> print_table(aov)
    """
    from scipy.stats import f

    # Check data
    _check_dataframe(dv=dv, between=between, data=data,
                     effects='between')

    # Reset index (avoid duplicate axis error)
    data = data.reset_index(drop=True)

    groups = list(data[between].unique())
    n_groups = len(groups)
    N = data[dv].size

    # Calculate sums of squares
    grp = data.groupby(between)[dv]
    # Between effect
    ssbetween = ((grp.mean() - data[dv].mean())**2 * grp.count()).sum()
    # Within effect (= error between)
    sserror = grp.apply(lambda x: (x - x.mean())**2).sum()

    # Calculate DOF, MS, F and p-values
    ddof1 = n_groups - 1
    ddof2 = N - n_groups
    msbetween = ssbetween / ddof1
    mserror = sserror / ddof2
    fval = msbetween / mserror
    p_unc = f(ddof1, ddof2).sf(fval)

    # Calculating partial eta-square
    # Similar to (fval * ddof1) / (fval * ddof1 + ddof2)
    np2 = ssbetween / (ssbetween + sserror)

    # Create output dataframe
    if not detailed:
        aov = pd.DataFrame({'Source': between,
                            'ddof1': ddof1,
                            'ddof2': ddof2,
                            'F': fval,
                            'p-unc': p_unc,
                            'np2': np2
                            }, index=[0])

        col_order = ['Source', 'ddof1', 'ddof2', 'F', 'p-unc', 'np2']
    else:
        aov = pd.DataFrame({'Source': [between, 'Within'],
                            'SS': [ssbetween, sserror],
                            'DF': [ddof1, ddof2],
                            'MS': [msbetween, mserror],
                            'F': [fval, np.nan],
                            'p-unc': [p_unc, np.nan],
                            'np2': [np2, np.nan]
                            })
        col_order = ['Source', 'SS', 'DF', 'MS', 'F', 'p-unc', 'np2']

    aov = aov.reindex(columns=col_order)
    aov.dropna(how='all', axis=1, inplace=True)
    # Export to .csv
    if export_filename is not None:
        _export_table(aov, export_filename)
    return aov

def mixed_anova(dv=None, within=None, between=None, data=None,
                correction='auto', remove_na=True, export_filename=None):
    """Mixed-design (split-plot) type II ANOVA.

    Results have been tested against R and JASP.

    Parameters
    ----------
    dv : string
        Name of column containing the dependant variable.
    within : string
        Name of column containing the within factor.
    between : string
        Name of column containing the between factor.
    data : pandas DataFrame
        DataFrame
    correction : string or boolean
        If True, return Greenhouse-Geisser corrected p-value.
        If 'auto' (default), compute Mauchly's test of sphericity to determine
        whether the p-values needs to be corrected.
    remove_na : boolean
        If True, automatically remove from the analysis subjects with one or
        more missing values::

            Ss    x1       x2       x3
            1     5.0      4.2      nan
            2     4.6      3.6      3.9

        In this example, if remove_na == True, Ss 1 will be removed from the
        ANOVA because of the x3 missing value. If False, the two non-missing
        values will be included in the analysis.
    export_filename : string
        Filename (without extension) for the output file.
        If None, do not export the table.
        By default, the file will be created in the current python console
        directory. To change that, specify the filename with full path.

    Returns
    -------
    aov : DataFrame
        ANOVA summary ::

        'Source' : Names of the factor considered
        'ddof1' : Degrees of freedom (numerator)
        'ddof2' : Degrees of freedom (denominator)
        'F' : F-values
        'p-unc' : Uncorrected p-values
        'np2' : Partial eta-square effect sizes
        'p-GG-corr' : Greenhouse-Geisser corrected p-values
        'W-Mauchly' : Mauchly statistic
        'X2-Mauchly' : Chi-square statistic of the Mauchly test
        'DF-Mauchly' : Degrees of freedom for the Mauchly test
        'p-Mauchly' : p-value of the Mauchly test
        'sphericity' : sphericity of the data (boolean)

    See Also
    --------
    anova : One-way ANOVA
    rm_anova : One-way repeated measures ANOVA

    Examples
    --------
    Compute a two-way mixed model ANOVA.

        >>> import pandas as pd
        >>> from pingouin import mixed_anova, print_table
        >>> df = pd.read_csv('dataset.csv')
        >>> aov = mixed_anova(dv='DV', within='Time', between='Group', data=df,
                             correction='auto', remove_na=False)
        >>> print_table(aov)
    """
    from scipy.stats import f
    # Check data
    _check_dataframe(dv=dv, within=within, between=between, data=data,
                     effects='interaction')
    # Remove NaN
    if remove_na and data[dv].isnull().values.any():
        data = _remove_rm_na(dv=dv, within=within, data=data)
    # Reset index (avoid duplicate axis error)
    data = data.reset_index(drop=True)

    # SUMS OF SQUARES
    grandmean = data[dv].mean()
    # Extract main effects of time and between
    mtime = rm_anova(dv=dv, within=within, data=data, correction=correction,
                     remove_na=False, detailed=True)
    mbetw = anova(dv=dv, between=between, data=data, detailed=True)
    # Extract SS total, residuals and interactions
    grp = data.groupby([between, within])[dv]
    sstotal = grp.apply(lambda x: (x - grandmean)**2).sum()
    # sst = residuals within + residuals between
    sst = grp.apply(lambda x: (x - x.mean())**2).sum()
    # Interaction
    ssinter = sstotal - (sst + mtime.loc[0, 'SS'] + mbetw.loc[0, 'SS'])
    sswg = mtime.loc[1, 'SS'] - ssinter
    sseb = sstotal - (mtime.loc[0, 'SS'] + mbetw.loc[0, 'SS'] + sswg + ssinter)

    # DEGREES OF FREEDOM
    n_obs = data.groupby(within)[dv].count().max()
    dftime = mtime.loc[0, 'DF']
    dfbetween = mbetw.loc[0, 'DF']
    dfeb = n_obs - data.groupby(between)[dv].count().count()
    dfwg = dftime * dfeb
    dfinter = mtime.loc[0, 'DF'] * mbetw.loc[0, 'DF']

    # MEAN SQUARES
    mseb = sseb / dfeb
    mswg = sswg / dfwg
    msinter = ssinter / dfinter

    # F VALUES
    fbetween = mbetw.loc[0, 'MS'] / mseb
    ftime = mtime.loc[0, 'MS'] / mswg
    finter = msinter / mswg

    # P-values
    pbetween = f(dfbetween, dfeb).sf(fbetween)
    ptime = f(dftime, dfwg).sf(ftime)
    pinter = f(dfinter, dfwg).sf(finter)

    # Effects sizes
    npsq_between = fbetween * dfbetween / (fbetween * dfbetween + dfeb)
    npsq_time = ftime * dftime / (ftime * dftime + dfwg)
    npsq_inter = ssinter / (ssinter + sswg)

    # Stats table
    aov = pd.concat([mbetw.drop(1), mtime.drop(1)], sort=False,
                    ignore_index=True)
    # Update values
    aov.rename(columns={'DF': 'DF1'}, inplace=True)
    aov.loc[0, 'F'], aov.loc[1, 'F'] = fbetween, ftime
    aov.loc[0, 'p-unc'], aov.loc[1, 'p-unc'] = pbetween, ptime
    aov.loc[0, 'np2'], aov.loc[1, 'np2'] = npsq_between, npsq_time
    aov = aov.append({'Source': 'Interaction',
                      'SS': ssinter,
                      'DF1': dfinter,
                      'MS': msinter,
                      'F': finter,
                      'p-unc': pinter,
                      'np2': npsq_inter
                      }, ignore_index=True)

    aov['DF2'] = [dfeb, dfwg, dfwg]
    col_order = ['Source', 'SS', 'DF1', 'DF2', 'MS', 'F', 'p-unc', 'np2',
                 'p-GG-corr', 'sphericity', 'W-Mauchly', 'X2-Mauchly',
                 'DF-Mauchly', 'p-Mauchly']

    aov = aov.reindex(columns=col_order)
    aov.dropna(how='all', axis=1, inplace=True)

    # Export to .csv
    if export_filename is not None:
        _export_table(aov, export_filename)
    return aov