TODO:

R1
-	get the Nyquist plot axis dimensions issue when $k=1$ fixed
-	figure out the failing of .pz with active elements

R2
-	make the frequency analysis stuff happen

```python
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
```

    WARNING: KICAD_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched.

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

from AC_2_Codes import *

sym.init_printing()

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

Software	Version

Python	3.7.6 64bit [GCC 7.3.0]

IPython	7.12.0

OS	Linux 4.19.104 microsoft standard x86_64 with debian bullseye sid

skidl	0.0.31.dev0

PySpice	1.4.3

lcapy	0.75.dev0

sympy	1.6.2

numpy	1.18.1

matplotlib	3.3.0

pandas	1.1.4

scipy	1.4.1

Mon Jan 18 15:33:46 2021 MST

# What is PZ anylsis

Pole-Zero analysis (.pz)does exactly what it says it does. It analysis the circuit between two ports and will return all the poles and or zero between the ports and that's it. That means with the resulting poles and zeros we can reconstruct the transfer function between the ports up to the gain term that is $$H(s)_{true}\propto \dfrac{\prod_n (s-a_n)}{\prod_m (s-b_m)}$$ where $a_n$ are the zeros and $b_n$ are the poles. Again this can't be stressed enough .pz does not recover the "gain" term $K$ that would turn the proportionality into an equality.

So what use is it? While that depends on what stage of the design cycle you're in and what is being analyzed. So for RLC elements, it's just going to tell us the poles and zeros where we know that the poles and zero do not move. But for active devices such as BJTs, FETs, etc we could cycle the .pz analysis and see how the .pz locations move with the bias. And while .pz is limited from a verification standpoint seeing as it will just confirm the pole-zero locations that should have been set in the design stage it can be of use when performing reverse engineering on an unknown circuit. Further during the design stage or when analyzing an unknown circuit the lack of $K$ is not a total handicap. Since even without $K$ we can perform root-locus analysis or we can sweep $K$ while comparing the resulting transfer function response to an .ac simulation to then determine $K$ when reverse engineering an unknown design.

So then let's go ahead and start looking at .pz and what we can do with that data.

# PZ analysis of an RC lowpass filter

For this first use case, we will use the RC lowpass filter that we developed in the last section bassed on the work of ALL ABOUT ELECTRONICS

```python
#instatate the rc_lowpass filter to
lowpassF=rc_lowpass(C_value=.1@u_uF, R_value=1@u_kOhm)
lowpassF.lcapy_self()
```

Lets then get the voltage transfer function for this filter topology

```python
H_rcl_gen=lowpassF.get_tf(with_values=False); H_rcl_gen
```

$$\frac{1}{C R \left(s + \frac{1}{C R}\right)}$$

The voltage transfer function for this topology shows that it has a single pole and the following gain term $K$

```python
H_rcl_gen.K
```

$$\frac{1}{C R}$$

Real quickly how lcapy is getting the full voltage-transfer function is based on

1.	Generating the symbolic Modified nodal analysis in the Laplace domain

2.	extracting the so-called Two-Port admittance parameters $Y$ from the Modified nodal analysis matrix

3.	finding the port 1 to port 2 voltage transfer function via $$H_v(s)=\dfrac{Y_{21}}{Y_{22}}$$ Which is just one way of doing it. Where more on two-port network theory and SPICE acquisition will be shown in the remaining sections of this chapter.

Lets now get the transfer function for this instinacs of the rc lowpass topology and isolate its $K$ term

```python
H_rcl=lowpassF.get_tf(with_values=True, ZPK=True); H_rcl
```

$$\frac{10000}{s + 10000}$$

```python
#K should always be real or where in trouble
K_rcl=np.real(H_rcl.K.cval); K_rcl
```

As with any SPICE simulation we have to instantiate our DUT in a circuit. However unlike DC's .tf we do not actually have to have any supplies but since we are also going be comparing the .ac simulation to what .pz we need a full circuit with a source to perform the .ac simulation

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

#create a 1V AC test source and attache to nets
vs=SINEV(ac_magnitude=1@u_V, dc_offset=5@u_V); vs['p', 'n']+=net_in, gnd

#attaceh term_0 to net_in and term_2 to net_out per scikit-rf convention all
#other terminals are grounded
lowpassF.SKiDl(net_in, gnd, net_out, gnd)

circ=generate_netlist()
print(circ)

```

    .title
    V1 In 0 DC 5V AC 1V 0.0rad SIN(0V 1V 50Hz 0s 0Hz)
    C1 Out 0 0.1uF
    R1 In Out 1kOhm

    No errors or warnings found during netlist generation.

```python
filter_responce=qfilter_explorer(circ, 'RC Low Pass Filter Responce');
```

```python
#this is the full filter tf response in comparison to the .ac sim
filter_responce.symbolic_tf(lowpassF)
```

## .tf does not get $K$

Lets be clear about this .tf will not yield $K$ in the generic case. It might get lucky but recalling that in DC simulations capcitors are treated as non existing elements there is no way that .tf will recover the $K$ for this topology where $K=\dfrac{1}{RC}$

```python
tf=easy_tf(circ)
```

```python
tf.dc_voltage_gain(vs, node(net_out), node(gnd))
tf.vg_results
```

      loc
      value
      units

      Output_resistance
      (Out-0)
      1000
      [Ohm]

      Input_resistance
      V1
      1e+20
      [Ohm]

      DC_Vgain
      (Out-0)/V1
      1
      [V/V]

# PZ ease

The following class like the rest in this book makes using the SPICE analysis easier and enhance it with the power of Python. But real quick let's look at the ngspice call for .pz (typically found in chapter 15 section 3)

```
.pz node1 node2 node3 node4 <transfer_type('vol'/'cur')> <analysis_type('pz'/'zer'/'pol')>
```

This differs from .tf in DC analysis where we had to specify a source for the input, where instead the input port terminals are specified by `node1` & `node2` which are the positive and negative terminals respectively. And similarly, the output port terminals are specified by `node3` & `node4`. Since .pz only requires the specification of the terminals to define the two-port network we can take advantage of this to look just at sat the feedback (aka $\beta$) network in circuits containing feedback structures.

Following the node arguments are the transfer type argument where if `vol` is used we are acquiring the poles and or zeros of voltage transfer function

$$H_v(s)=\dfrac{V_o}{V_i}$$

else, if `cur` is used we are acquiring the Transimpedance (aka Transfer Impedance)
$$H_F(s)=\dfrac{V_o}{I_i}$$
, were again when using .pz we are only acquiring the poles and or zeros that make up the respective transfer function not the transfer function as a whole.

Finlay the last argument `analysis_type` controls what we are acquiring from the .pz analysis. While typically we leave it as `pz` to get both the poles and zeros there are times it might not be possible to get both or the poles and zero have to be acquired separately. Where in that case we can use `pol` to get just the poles and `zer` to get just the zeros

Below the class, `pz_ease` is designed to perform the .pz analysis with additional methods to analyze the results. And in both it's instantiation and in serval of its methods, the value of $K$ can be feed into it if known.

```python
#%%writefile -a AC_2_Codes.py
#chapteer 2 section 4 pz_ease class
#class to perform .pz simulations with a bit more grace
#with some additional built-in analysis tools

class pz_ease(ac_representation_tool, eecomplex_plot_templets):
    def __init__(self, circ_netlist_obj, K=1.0):
        """
        Class to perform Pole Zero (.pz) SPICE simulation with grace

        Args:
            circ_netlist_obj (pyspice.Spice.Netlist.Circuit): the Netlist circuit produced
                from SKiDl's `generate_netlist()`

            K (float/int; 1): the gain; must be manually put in or found from .tf analysis

        Returns:

        """
        self.circ_netlist_obj=circ_netlist_obj

        assert (type(K)==float) or (type(K)==int), 'K must be a float or int'
        self.K=K

        #dic of allowed pz control statements
        self.allowed_control_statments={'voltage':'vol', 'current':'cur',
                                       'pole-zero':'pz', 'zeros':'zer', 'poles':'pol'}

    def pz_def_ports(self, port_0_pos_term, port_0_neg_term, port_1_pos_term, port_1_neg_term, display_table=False):
        """
        Method to set the Port terminals for the two-port section of the circuit under test
        where all inputs must be nodes in the circuit under test

        Terminals:
        port_0_pos_term, port_0_neg_term, port_1_pos_term, port_1_neg_term

        Port & Terminals are defined via:
        ```
                        Left_Port - Two-Port Section under Test - Right_Port
                                        +-------------+
        Postive Port0   port_0_pos_term-| DUT Section |-port_1_pos_term     Postive Port1
        Negtive Port0   port_0_neg_term-|             |-port_1_neg_term     Negtive Port1
                                        +-------------+
        ```
        Args:
            display_table (bool; False): when true will display the generated `self.control_df` below
                this method call in a jupyter notebook like environment

        Returns:
            Settings are recoded in `self.control_df` rows: `'port_0_terms+-'` & `'port_1_terms+-'`
        """

        assert port_0_pos_term in self.circ_netlist_obj.node_names, f'`{port_0_pos_term}` is not a node in the circuit under test'
        self.port_0_pos_term=port_0_pos_term

        assert port_0_neg_term in self.circ_netlist_obj.node_names, f'`{port_0_neg_term}` is not a node in the circuit under test'
        self.port_0_neg_term=port_0_neg_term

        assert port_1_pos_term in self.circ_netlist_obj.node_names, f'`{port_1_pos_term}` is not a node in the circuit under test'
        self.port_1_pos_term=port_1_pos_term

        assert port_1_neg_term in self.circ_netlist_obj.node_names, f'`{port_1_neg_term}` is not a node in the circuit under test'
        self.port_1_neg_term=port_1_neg_term

        #record the results in table
        self._build_control_table(display_table)

    def pz_mode_set(self, tf_type='voltage', pz_acu='pole-zero', display_table=False):
        """
        Method to set the pole-zero analysis controls

        Args:
            tf_type (str; 'voltage'): the tf for wich the poles and zeros fit to
                if `voltage` the tf is of the form V_o/V_i else if `current` in the form of
                V_o/I_i

            pz_acu (str; 'pole-zero'): if `pole-zero` will attempt to get all the poles and zeros for the
                specfied transfer function; else if `zeros` or `poles` will get just the respective zeros
                or poles

            display_table (bool; False): when true will display the generated `self.control_df` below
                this method call in a jupyter notebook like environment

        Returns:
            Settings are recoded in `self.control_df` rows: `'tf_type'` & `'acqui_mode'`

        """
        assert tf_type in self.allowed_control_statments.keys(), f'`{tf_type}` is not `voltage` or `current`'
        self.tf_type=tf_type

        assert pz_acu in self.allowed_control_statments.keys(), f'`{pz_acu}` is not `pole-zero` or `poles` or `zeros`'
        self.pz_acu=pz_acu

        #record the results in table
        self._build_control_table(display_table)

    def _build_control_table(self, display_table=True):
        """
        Internal method to build a pz control table to display pz simulation settings

        Args:
            display_table (bool; True): when true will display the generated `self.control_df` below
                this method call in a jupyter notebook like environment

        Returns:
            creates dataframe table `self.control_df` that records pz simulation controls
            if `display_table` is true will force showing under jupyter notebook cell

        """

        self.control_df=pd.DataFrame(columns=['value'],
                                     index=['tf_type',
                                            'acqui_mode',
                                            'port_0_terms+-',
                                            'port_1_terms+-'
                                           ])
        if hasattr(self, 'tf_type'):
            self.control_df.at['tf_type']=self.tf_type

        if hasattr(self, 'pz_acu'):
            self.control_df.at['acqui_mode']=self.pz_acu

        if hasattr(self, 'port_0_pos_term') and hasattr(self, 'port_0_neg_term') :
            self.control_df.at['port_0_terms+-', 'value']=[self.port_0_pos_term,  self.port_0_neg_term]

        if hasattr(self, 'port_1_pos_term') and hasattr(self, 'port_1_neg_term') :
            self.control_df.at['port_1_terms+-', 'value']=[self.port_1_pos_term,  self.port_1_neg_term]

        self.control_df.index.name='pz_sim_control'

        if display_table:
            display(self.control_df)

    def do_pz_sim(self, display_table=False):
        """
        Method to perform the pole-zero simulation based on values stored in self.control_df
        If the simulation does not converge will give a warning with a basic debug action
        but will set `self.pz_values` to empty dict.

        TODO:
            - add simulation kwargs
            - flush out exception handling
        """

        attriputs_to_check=['port_0_pos_term', 'port_0_neg_term', 'port_1_pos_term', 'port_1_neg_term',
                           'tf_type', 'pz_acu']

        for i in attriputs_to_check:
            if hasattr(self, i):
                pz_is_go=True
            else:
                pz_is_go=False
                warnings.warn(f'{i} has not been set; pole-zero simulation will not procdede till set')

        if pz_is_go:
            self.sim=self.circ_netlist_obj.simulator()
            #I cant catch the warning when it hangs so going to have to do this
            self.pz_values={}

            try:
                self.pz_values=self.sim.polezero(
                    node1=self.port_0_pos_term,
                    node2=self.port_0_neg_term,
                    node3=self.port_1_pos_term,
                    node4=self.port_1_neg_term,
                    tf_type=self.allowed_control_statments[self.tf_type],
                    pz_type=self.allowed_control_statments[self.pz_acu]
                )

                self._record_pz_results(display_table)

            except pspice.Spice.NgSpice.Shared.NgSpiceCommandError:
                self.pz_values={}
                warnings.warn("""PZ analysis did not converge with the current setting:
                start by changing the tf type (self.tf_type) and pz acusisiton type (self.pz_acu) """)

    def _record_pz_results(self, display_table=True):
        """
        Internal method to record the PZ results to a dataframe

        Args:
            display_table (bool; True): when true will display the generated `self.control_df` below
                this method call in a jupyter notebook like environment

        Returns:
            creates dataframe table `self.pz_results_DF` that records pz simulation results
            if `display_table` is true will force showing under jupyter notebook cell

        """
        self.pz_results_DF=pd.DataFrame(columns=['Type', 'Values'])

        if hasattr(self.pz_values, 'nodes'):
            for k, v in self.pz_values.nodes.items():
                self.pz_results_DF.at[len(self.pz_results_DF)]=k, v.as_ndarray()[0]

        if display_table:
            display(self.pz_results_DF)

    def get_pz_sym_tf(self, dec_round=None, overload_K=None):
        """
        Method to get the symbolic transfer function via lacpy

        Args:
            dec_round (int; None): contorl to `np.around`'s `decimals` argument
                if left `None` np.around will not be used

            overload_K (float/int; None): if not `None` will overload the DC
                gain constant stored in `self.K`

        Returns:
            if `self.pz_results_DF` exists return the symbolic transfer function in the s
            dominan in `self.sym_tf`
        """
        if overload_K!=None:
            assert (type(overload_K)==float) or (type(overload_K)==int), 'K must be a float or int'
            self.K=overload_K

        if hasattr(self, 'pz_results_DF')!=True:
            warnings.warn('no poles/zero recorded run `self.do_pz_sim`')
        else:
            zeros_B=np.empty(0)
            poles_A=np.empty(0)
            for index, row in self.pz_results_DF.iterrows():
                if 'zero' in row['Type']:
                    zeros_B=np.hstack((zeros_B, row['Values']))
                elif 'pole' in row['Type']:
                    poles_A=np.hstack((poles_A, row['Values']))

            if dec_round!=None:
                zeros_B=np.around(zeros_B, dec_round)
                poles_A=np.around(poles_A, dec_round)

            self.zeros_B=zeros_B; self.poles_A=poles_A
            #wish I didn't have to do this
            zeros_B=zeros_B.tolist(); poles_A=poles_A.tolist()

            #use lcapy to get the symbolic tf
            self.sym_tf=kiwi.zp2tf(zeros_B, poles_A, K=self.K)
            #use simplify because if in pzk it does weird things with j that
            #lambdfy has issues with
            self.sym_tf=self.sym_tf.simplify()

    def plot_pz_loc(self, ax=None, title='', unitcircle=False):
        """
        uses lcapy's `plot_pole_zero` in https://github.com/mph-/lcapy/blob/6e42983d6b77954e694057d61045bd73d17b4616/lcapy/plot.py#L12
        to plot the poles and zero locations on a Nyquist chart

        Args:
            axs (list of matplotlib axis; None): If left None will create a new plot, else must
                be a list of matplotlib subplots axis to be added to where the first entry
                will be the magnitude axis, and the second will be the phase axis

            title (str; ''): Subplot title string

            unitcircle (bool; False): when True will plot the unit circle on the resulting plot

        Returns:
            Returns a real-imag with Poles and Zero map, and if an axis was passed to `ax` will be modified
            with the pole-zero map

        """
        axs=ax or plt.gca()
        if hasattr(self, 'sym_tf')==False:
            warnings.warn("""Trying to get symbolic transfer function from `self.get_pz_sym_tf`
            thus you will get what you get""")
            self.get_pz_sym_tf()

        self.sym_tf.plot(axes=axs, unitcircle=unitcircle,
                         #wish there be a better way to do this
                        label="'X'=pole; 'O'=zero")

        axs.axhline(0, linestyle='--', linewidth=2.0, color='black')
        axs.axvline(0, linestyle='--', linewidth=2.0, color='black')
        axs.set_xlabel('Real'); axs.set_ylabel('Imag')

        axs.legend()

        if title!='':
            title=' of '+title
        axs.set_title(f'Pole-Zero locations plot{title}');

    def plot_3d_laplce(self, title=''):
        """
        Creates 3d plots of the Laplace space of the transfer function, one for the mag
        the other for the phase in degrees unwrapped

        Args:
                title (str; ''): Subplot title string

        Returns:
            returns a 3d plot with the left subplot being the  mag and the right being the phase

        TODO:
            - get the freaking color bar into a clean location when working with 3d plots
            - merge phase as color into mag see the physics video by eugene on Laplace
        """
        if hasattr(self, 'sym_tf')==False:
            warnings.warn("""Trying to get symbolic transfer function from `self.get_pz_sym_tf`
            thus you will get what you get""")
            self.get_pz_sym_tf()

        #import the additnal matplotlib featuers for 3d
        from mpl_toolkits.mplot3d import Axes3D
        from matplotlib import cm

        #stole this off lcapy's plot_pole_zero
        #https://github.com/mph-/lcapy/blob/7c4225f2159aa33398dac481041ed538169b7058/lcapy/plot.py

        #check self.sys_tf is good to be used
        sys_tf_syms=self.sym_tf.symbols
        assert len(sys_tf_syms)==1 and ('s' in sys_tf_syms.keys()), 'trasfer function must be laplce form and only have `s` as a free symbol'

        #lambdfy the tf
        sys_tf_lam=sym.lambdify(kiwi.s, self.sym_tf.canonical(), 'numpy', dummify=False)

        #get the plot bounds
        #stole this off lcapy's plot_pole_zero
        #https://github.com/mph-/lcapy/blob/7c4225f2159aa33398dac481041ed538169b7058/lcapy/plot.py

        poles = self.sym_tf.poles()
        zeros = self.sym_tf.zeros()
        try:
            p = np.array([p.cval for p in poles.keys()])
            z = np.array([z.cval for z in zeros.keys()])
        except ValueError:
            raise TypeError('Cannot get poles and zeros of `self.sym_tf')
        a = np.hstack((p, z))
        x_min = a.real.min()
        x_max = a.real.max()
        y_min = a.imag.min()
        y_max = a.imag.max()

        x_extra, y_extra = 3.0, 3.0

        # This needs tweaking for better bounds.
        if len(a) >= 2:
            x_extra, y_extra = 0.1 * (x_max - x_min), 0.1 * (y_max - y_min)
        if x_extra == 0:
            x_extra += 1.0
        if y_extra == 0:
            y_extra += 1.0

        x_min -= 0.5 * x_extra
        x_max += 0.5 * x_extra
        y_min -= 0.5 * y_extra
        y_max += 0.5 * y_extra

        #the input domain
        RealRange=np.linspace(x_min, x_max, 100); ImagRange=np.linspace(y_min, y_max, 100)
        sr, si=np.meshgrid(RealRange, ImagRange)
        s_num=sr+1j*si

        #plot this
        fig = plt.figure()

        #mag 3d plot
        ax3d_mag = fig.add_subplot(121, projection='3d')

        XmagPlot=ax3d_mag.plot_surface(sr, si, np.abs(sys_tf_lam(s_num)), alpha=0.5,
                                 cmap=cm.coolwarm, antialiased=False)
        ax3d_mag.set_xlabel(r'$\sigma$'); ax3d_mag.set_ylabel(r'$j\omega$'), ax3d_mag.set_zlabel(r'$|X|$')
        fig.colorbar(XmagPlot, shrink=0.5, aspect=5)

        #phase 3d plot
        ax3d_phase = fig.add_subplot(122, projection='3d')

        XphasePlot=ax3d_phase.plot_surface(sr, si, angle_phase_unwrap(sys_tf_lam(s_num)), alpha=0.5,
                                 cmap=cm.coolwarm, antialiased=False)
        ax3d_phase.set_xlabel(r'$\sigma$'); ax3d_phase.set_ylabel(r'$j\omega$'), ax3d_phase.set_zlabel(r'$ang(X)$')
        fig.colorbar(XphasePlot, shrink=0.5, aspect=5)

        plt.tight_layout()

        if title!='':
            title=' of '+title
        ax3d_mag.set_title(f'3D Mag Laplace plot{title}');
        ax3d_phase.set_title(f'3D Phase_deg Laplace plot{title}');

    def scipy_pzk(self, dec_round=None, overload_K=None):
        """
        Method to create to generate the Numerator (a) and Denominator (b)
        arrays to use within the scipy signal framework see
        https://docs.scipy.org/doc/scipy/reference/signal.html
        &
        https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.zpk2tf.html#scipy.signal.zpk2tf

        Args:
            dec_round (int; None): contorl to `np.around`'s `decimals` argument
                if left `None` np.around will not be used

            overload_K (float/int; None): if not `None` will overload the DC
                gain constant stored in `self.K`

        Returns:
            if `self.pz_results_DF` exists tries to return the coefficients for an lti
            filter for use in scipy's signal library in a dictionary in `self.scipy_tfcoef`
        """
        if overload_K!=None:
            assert (type(overload_K)==float) or (type(overload_K)==int), 'K must be a float or int'
            self.K=overload_K

        if hasattr(self, 'pz_results_DF')!=True:
            warnings.warn('no poles/zero recorded run `self.do_pz_sim`')
        else:
            zeros_B=np.empty(0)
            poles_A=np.empty(0)
            for index, row in self.pz_results_DF.iterrows():
                if 'zero' in row['Type']:
                    zeros_B=np.hstack((zeros_B, row['Values']))
                elif 'pole' in row['Type']:
                    poles_A=np.hstack((poles_A, row['Values']))

            if dec_round!=None:
                zeros_B=np.around(zeros_B, dec_round)
                poles_A=np.around(poles_A, dec_round)

            b, a=scipy_zpk2tf(zeros_B, poles_A, self.K)
            self.scipy_tfcoef={'b':b, 'a':a}

    def get_sym_freq_resp(self, freq_vec=None):
        """
        method to get the symbolic transfer function  response

        Args:
            freq_vec (numpy array or pandas series; None): frequencies
            to generate results from the generated symbolic transfer function
            if `None` will come up with a freancy vector inside via `np.logspace(-1, 12, 12*10*10)`

        Returns:
            will store frequency vector as the index in `self.pz_symresults_DF`
            where the response of the symbolic transfer function will be stored
            in `self.pz_symresults_DF['pzk_symres_[V]']` from there will
            use the inheritance from `ac_representation_tool` to then
            generate `self.ac_sim_real_DF['pzk_symres_[V]'], self.ac_sim_imag_DF['pzk_symres_[V]']`, ect

        """
        if hasattr(self, 'sym_tf')==False:
            warnings.warn("""Trying to get symbolic transfer function from `self.get_pz_sym_tf`
            thus you will get what you get""")
            self.get_pz_sym_tf()

        if freq_vec==None:
            self.freq_vec=np.logspace(-1, 12, 12*10*10)

        self.pz_symresults_DF=pd.DataFrame(index=self.freq_vec)
        self.pz_symresults_DF.index.name='freq[Hz]'

        self.pz_symresults_DF['pzk_symres_[V]']=self.sym_tf.frequency_response(self.pz_symresults_DF.index.values).astype('complex')

        ac_representation_tool.__init__(self, self.pz_symresults_DF)

        #just make the reps
        self.make_real_imag()
        self.make_mag_phase()

    def plot_nyquist_with_pz(self, ax=None, title='', unitcircle=False):
        """
        plotting utility to plot the pole-zero location on top of a Nyquist
        a plot of the response of the symbolic transfer function

        Args:
            ax (matplotlib axis; None): If left None will create a new plot, else must
                be a matplotlib subplot axis to be added to

            title (str; ''): Subplot title string

        Returns:
            Returns a Nyquist plot with the pole-zero locations superimposed on top of,
            and if an axis was passed to `ax` will be modified

        """
        axs=ax or plt.gca()
        if hasattr(self, 'ac_sim_real_DF')==False:
            warnings.warn("""Trying to get the real imag data from `self.get_sym_freq_resp`,
            you get what you get""")
            self.get_sym_freq_resp()

        self.nyquist_plot_templet(self.ac_sim_real_DF['pzk_symres_[V]'], self.ac_sim_imag_DF['pzk_symres_[V]'],
                           ax=axs)
        self.plot_pz_loc(ax=axs, unitcircle=unitcircle)

        if title!='':
            title=' of '+title
        axs.set_title(f'Nyquist with Pole-Zero locations plot{title}');

    def plot_bode_from_pz(self, ax=None, title=''):
        """
        plotting utility to plot single mag and phase on a single bode plot
        for the response of the symbolic transfer function

        Args:
            ax (matplotlib axis; None): If left None will create a new plot, else must
                be a matplotlib subplot axis to be added to

            title (str; ''): Subplot title string

        Returns:
            Returns a bode plot from the symbolic transfer function,
            and if an axis was passed to `ax` will be modified
        """
        axs=ax or plt.gca()
        if hasattr(self, 'ac_sim_real_DF')==False:
            warnings.warn("""Trying to get the mag phase data from `self.get_sym_freq_resp`,
            you get what you get""")
            self.get_sym_freq_resp()

        self.bode_plot_one_templet(self.ac_sim_mag_DF.index, self.ac_sim_mag_DF['pzk_symres_[V][dB]'], self.ac_sim_phase_DF['pzk_symres_[V][deg]'],
                           ax=axs)

        if title!='':
            title=' of '+title
        axs.set_title(f'Bode plot from Pole-Zero anylsis plot{title}');

    def plot_nichols_from_pz(self, ax=None, title=''):
        """
        plotting utility to plot the Nichols chart
        for the response of the symbolic transfer function

        Args:
            ax (matplotlib axis; None): If left None will create a new plot, else must
                be a matplotlib subplot axis to be added to

            title (str; ''): Subplot title string

        Returns:
            Returns a Nichols chart plot from the symbolic transfer function,
            and if an axis was passed to `ax` will be modified
        """
        axs=ax or plt.gca()

        if hasattr(self, 'ac_sim_real_DF')==False:
            warnings.warn("""Trying to get the mag phase data from `self.get_sym_freq_resp`,
            you get what you get""")
            self.get_sym_freq_resp()

        self.nichols_plot_templet(self.ac_sim_mag_DF['pzk_symres_[V][dB]'], self.ac_sim_phase_DF['pzk_symres_[V][deg]'],
                           ax=axs)

        if title!='':
            title=' of '+title
        axs.set_title(f'Nichols plot from Pole-Zero anylsis plot{title}');

```

To use `pz_ease` we instantiate it by passing in the circuit under test at the time of creation. The instantiation has another argument to pass in $K$ if known *a prior* if just the moment we will leave $K=1$ to demonstrate what .pz and `pz_ease` can do when $K$ is not known

```python
pz=pz_ease(circ)
```

next, we define the port terminals and set the control for the voltage transfer function and to get both poles and zero which is the default case.

```python
pz.pz_def_ports('In', '0', 'Out', '0')
pz.pz_mode_set( display_table=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>value</th>
    </tr>
    <tr>
      <th>pz_sim_control</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>tf_type</th>
      <td>voltage</td>
    </tr>
    <tr>
      <th>acqui_mode</th>
      <td>pole-zero</td>
    </tr>
    <tr>
      <th>port_0_terms+-</th>
      <td>[In, 0]</td>
    </tr>
    <tr>
      <th>port_1_terms+-</th>
      <td>[Out, 0]</td>
    </tr>
  </tbody>
</table>
</div>

And so now we run the .pz simulation

```python
pz.do_pz_sim(display_table=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Type</th>
      <th>Values</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>pole(1)</td>
      <td>(-10000+0j)</td>
    </tr>
  </tbody>
</table>
</div>

## Results when $K=1$

And we see that we get the pole that we know exists with the value we expect. So now then we can feed the results into Lcapy's zp2tf to generate the symbolic transfer function. Where we will keep $K=1$ for the moment

```python
pz.get_pz_sym_tf()
H_pzk1=pz.sym_tf; H_pzk1
```

$$\frac{1}{s + 10000.0}$$

We can now plot the pole zero locations

```python
pz.plot_pz_loc()
```

Generate the 3d plot of the Laplace domain response of this filter. Where the Laplace variable $s$ is equal to
$$s=\sigma +j\omega$$
where $j\omega$ is the angular frequency response and when $\sigma$ is zero $s$ gives yields the Fourier transform response of the system. Otherwise $\sigma=1/\tau$ the decay response of the system. For more info review the section ["Transfer Function Analysis"](https://control.com/textbook/ac-electricity/transfer-function-analysis/) from the controls book at https://control.com/

```python
pz.plot_3d_laplce()
```

We then can get the symbolic transfer function’s frequency response and view the Nyquist interpretation over the poles and zeros. Though when $K=1$ this can have issues in the plotting.

```python
#pz.plot_nyquist_with_pz()
#dont get why this is not working with K=1
```

As well as plotting the symbolic transfer function’s Bode and Nichols plot interpretation of its response.

```python
pz.plot_bode_from_pz()
```

```python
pz.plot_nichols_from_pz()
```

And finally, besides generating the symbolic transfer function, pz_ease can also use scipy's signal module to generate a dictionary containing the transfer functions numerator and denominator terms from the pole zeros and $K$ to the use within the scipy signal module or in the python controls library.

```python
pz.scipy_pzk(); pz.scipy_tfcoef
```

    {'b': array([1.]), 'a': array([1.e+00, 1.e+04])}

## Results with Proper $K$

Now giggle and grin lets see what our lowpass filtes full transfer function responce looks like when we know $K$ using the SPICE estracted poles and zeros from .pz anylsis. Where after overriding the $K$ in `pz_ease` we need to rerun `pz_ease.get_sym_freq_resp()` to regenerate the symploic transfer functions respnonce for the new sympolic transfer function found after $K$ what overridden

```python
pz.get_pz_sym_tf(overload_K=K_rcl)
pz.get_sym_freq_resp()
H_pzk1=pz.sym_tf; H_pzk1
```

$$\frac{10000}{s + 10000.0}$$

```python
pz.plot_3d_laplce()
```

```python
# figure out this strange matplotlib value error
#pz.plot_nyquist_with_pz()
```

```python
pz.plot_bode_from_pz()
```

```python
pz.plot_nichols_from_pz()
```

The results show that with an appropriate value of $K$ set we get the value that matches the .ac simulation results.

## Asking too much from .pz

Lets now see what happens when .pz does not work

```python
pz.pz_mode_set(pz_acu='zeros', display_table=True)
pz.do_pz_sim(display_table=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>value</th>
    </tr>
    <tr>
      <th>pz_sim_control</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>tf_type</th>
      <td>voltage</td>
    </tr>
    <tr>
      <th>acqui_mode</th>
      <td>zeros</td>
    </tr>
    <tr>
      <th>port_0_terms+-</th>
      <td>[In, 0]</td>
    </tr>
    <tr>
      <th>port_1_terms+-</th>
      <td>[Out, 0]</td>
    </tr>
  </tbody>
</table>
</div>

    Error: There are no vectors currently active.
    /home/iridium/anaconda3/lib/python3.7/site-packages/ipykernel_launcher.py:181: UserWarning: PZ analysis did not converge with the current setting:
                    start by changing the tf type (self.tf_type) and pz acusisiton type (self.pz_acu)

In this first error case, the pyspice ngspice wrapper catches the ngspice error that in this case, we know is due from trying to calculate zeros from a circuit we know does not have any zeros. We can also run into cases where instead of throwing a blatant error ngspice will throw a warning when it cant converge to a solution. In which case at the moment `pz_ease` won't display the warning but will set `pz_ease.pz_values` to an empty dictionary.

# Pole-Zero analysis of a Series Band Reject filter

Here we get the pole-zero analysis results of one of the filter examples from the previous section where we know that the transfer function has a zero coefficient term in its numerator to demonstrate the utility of `pz_ease` to formulate the transfer function form the .pz results.

```python
#instantiate the rlc_bandstop filter
bandstopRLC_s=rlc_series_bandstop(L_value=8.33e-5@u_H, C_value=2.7e-7@u_F, R_value=50@u_Ohm)
bandstopRLC_s.lcapy_self()
```

```python
H_rlcs_bs=bandstopRLC_s.get_tf(with_values=True, ZPK=False).ratfloat(); H_rlcs_bs
```

$$\frac{s^{2} + 44462229336.1789}{s^{2} + 600240.096038415 s + 44462229336.1789}$$

```python
K_rlcs_bs=np.real(H_rlcs_bs.K.cval); K_rlcs_bs
```

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

#create a 1V AC test source and attache to nets
vs=SINEV(ac_magnitude=1@u_V); vs['p', 'n']+=net_in, gnd

#attaceh term_0 to net_in and term_2 to net_out per scikit-rf convention all
#other terminals are grounded
bandstopRLC_s.SKiDl(net_in, gnd, net_out, gnd)

circ=generate_netlist()
print(circ)
```

    .title
    V1 In 0 DC 0V AC 1V 0.0rad SIN(0V 1V 50Hz 0s 0Hz)
    L1 Out N_1 8.33e-05H
    C1 N_1 0 2.7e-07
    R1 In Out 50Ohm

    No errors or warnings found during netlist generation.

```python
filter_responce=qfilter_explorer(circ, 'RLC Series Bandstop Filter Responce');
```

```python
filter_responce.symbolic_tf(bandstopRLC_s)
```

Where again we will invoke the .tf of this function to show that .tf will not give us $K$ reliably only in this case serendipitously. So don't rely on it for finding $K$.

```python
tf=easy_tf(circ)
```

```python
tf.dc_voltage_gain(vs, node(net_out), node(gnd))
tf.vg_results
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>loc</th>
      <th>value</th>
      <th>units</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>Output_resistance</th>
      <td>(Out-0)</td>
      <td>50</td>
      <td>[Ohm]</td>
    </tr>
    <tr>
      <th>Input_resistance</th>
      <td>V1</td>
      <td>1e+20</td>
      <td>[Ohm]</td>
    </tr>
    <tr>
      <th>DC_Vgain</th>
      <td>(Out-0)/V1</td>
      <td>1</td>
      <td>[V/V]</td>
    </tr>
  </tbody>
</table>
</div>

Then getting the .pz results

```python
pz=pz_ease(circ, K=K_rlcs_bs)
```

```python
pz.pz_def_ports('In', '0', 'Out', '0')
pz.pz_mode_set( display_table=True)

pz.do_pz_sim(display_table=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>value</th>
    </tr>
    <tr>
      <th>pz_sim_control</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>tf_type</th>
      <td>voltage</td>
    </tr>
    <tr>
      <th>acqui_mode</th>
      <td>pole-zero</td>
    </tr>
    <tr>
      <th>port_0_terms+-</th>
      <td>[In, 0]</td>
    </tr>
    <tr>
      <th>port_1_terms+-</th>
      <td>[Out, 0]</td>
    </tr>
  </tbody>
</table>
</div>

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Type</th>
      <th>Values</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>zero(2)</td>
      <td>-210860.69j</td>
    </tr>
    <tr>
      <th>1</th>
      <td>zero(1)</td>
      <td>210860.69j</td>
    </tr>
    <tr>
      <th>2</th>
      <td>pole(2)</td>
      <td>(-86555.51+0j)</td>
    </tr>
    <tr>
      <th>3</th>
      <td>pole(1)</td>
      <td>(-513684.6+0j)</td>
    </tr>
  </tbody>
</table>
</div>

Where the resulting transfer function the .pz anylsis is

```python
pz.get_pz_sym_tf()
pz.sym_tf.canonical()
```

$$\frac{1.0 s^{2} + 44462229532.9727}{1.0 s^{2} + 600240.1015625 s + 44462230867.489}$$

and recaling the anylsitical transfer function as the reference

```python
H_rlcs_bs.ratfloat()
```

$$\frac{s^{2} + 44462229336.1789}{s^{2} + 600240.096038415 s + 44462229336.1789}$$

We can then look at the error, wich given the size of the coefficients is small

```python
#numerator error
N_error=H_rlcs_bs.N-pz.sym_tf.canonical().N
D_error=H_rlcs_bs.D-pz.sym_tf.canonical().D
print(f'Numerator Delta: {N_error}')
print(f'Denomator Delta: {D_error}')
```

    Numerator Delta: -196.793739318848
    Denomator Delta: -0.00552408467046916*s - 1531.31009674072

The so the results for scipy signal the .pz aquared pole-zeros yields

```python
pz.scipy_pzk(); pz.scipy_tfcoef
```

    {'b': array([1.00000000e+00, 0.00000000e+00, 4.44622295e+10]),
     'a': array([1.00000000e+00, 6.00240102e+05, 4.44622309e+10])}

```python
pz.plot_3d_laplce()
```

```python
pz.plot_nyquist_with_pz()
```

Note that because of the larger space of the poles of zero for this system under test the results of the frequency response is concentrated around (0,0) and shows up as a blip.

```python
pz.plot_bode_from_pz()
```

```python
pz.plot_nichols_from_pz()
```

# Pole-Zero analysis of a lattice All-Pass filter¶

We also know that from the previous section that the lcapy generated transfer function was not correct to the .ac simulation results (properly due to this authors lcapy usage error) Thus let's take a look at what .pz gives us

```python
#instatate the all-pass lattice filter to
allpasslat_lf=lc_balanced_allpass_lowfreq_lattice_filt()
allpasslat_lf.lcapy_self()
```

```python
#get this filters abstract transfer function
allpasslat_lf.get_tf(with_values=True, ZPK=False)
```

$$\frac{1 - s^{2}}{s^{2} + 1}$$

```python
reset()
#create the nets; the last one is needed to deal with singularity issues when dealing with lattice circuits and ground
net_in=Net('In'); net_out=Net('Out'); net_outlower=Net('Out2')
#create a 1V AC test source and attache to nets
vs=SINEV(ac_magnitude=1@u_V); vs['p', 'n']+=net_in, gnd
#net_in+=dummy_1[2]

#attaceh term_0 to net_in and term_2 to net_out per scikit-rf convention all
#other terminals are grounded
#but need to add dummy resistors to deal with singular issues and get solvable matric
dummy_botin=R(value=0, ref='dummy')
dummy_botin[1]+=gnd

dummy_botout=R(value=0, ref='dummy')
dummy_botout[2]+=gnd
allpasslat_lf.SKiDl(net_in, dummy_botin[2], net_out, dummy_botout[1])

circ=generate_netlist()
print(circ)

```

    .title
    V1 In 0 DC 0V AC 1V 0.0rad SIN(0V 1V 50Hz 0s 0Hz)
    Rdummy 0 N_1 0
    Rdummy_1 N_2 0 0
    L1 In Out 1H
    L2 N_1 N_2 1H
    C1 Out N_1 1
    C2 In N_2 1

    No errors or warnings found during netlist generation.

```python
filter_responce=qfilter_explorer(circ, 'LC All-Pass Low Frequcy Lattice Filter');
```

```python
filter_responce.symbolic_tf(allpasslat_lf)
```

```python
pz=pz_ease(circ, K=1)
```

```python
pz.pz_def_ports('In', '0', 'Out', '0')
pz.pz_mode_set( display_table=True)

pz.do_pz_sim(display_table=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>value</th>
    </tr>
    <tr>
      <th>pz_sim_control</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>tf_type</th>
      <td>voltage</td>
    </tr>
    <tr>
      <th>acqui_mode</th>
      <td>pole-zero</td>
    </tr>
    <tr>
      <th>port_0_terms+-</th>
      <td>[In, 0]</td>
    </tr>
    <tr>
      <th>port_1_terms+-</th>
      <td>[Out, 0]</td>
    </tr>
  </tbody>
</table>
</div>

    Warning: Pole-zero iteration limit reached; giving up after 282 trials

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>Type</th>
      <th>Values</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>zero(3)</td>
      <td>(-0.002000004+0j)</td>
    </tr>
    <tr>
      <th>1</th>
      <td>zero(2)</td>
      <td>(-499.999-500j)</td>
    </tr>
    <tr>
      <th>2</th>
      <td>zero(1)</td>
      <td>(-499.999+500j)</td>
    </tr>
    <tr>
      <th>3</th>
      <td>pole(4)</td>
      <td>(-0.000499999-0.9999994j)</td>
    </tr>
    <tr>
      <th>4</th>
      <td>pole(3)</td>
      <td>(-0.000499999+0.9999994j)</td>
    </tr>
    <tr>
      <th>5</th>
      <td>pole(2)</td>
      <td>(-0.002000004+0j)</td>
    </tr>
    <tr>
      <th>6</th>
      <td>pole(1)</td>
      <td>(-999.999+0j)</td>
    </tr>
  </tbody>
</table>
</div>

```python
pz.scipy_pzk(); pz.scipy_tfcoef
```

    {'b': array([1.00000000e+00, 9.99999986e+02, 5.00000993e+05, 1.00000001e+03]),
     'a': array([1.00000000e+00, 1.00000202e+03, 4.00000013e+00, 1.00000208e+03,
            2.00000022e+00])}

```python
pz.get_pz_sym_tf()
H=pz.sym_tf; H.canonical()
```

$$\frac{1.0 s^{2} + 999.997985839844 s + 499998.992920936}{1.0 s^{3} + 1000.00002343545 s^{2} + 1.99999603336759 s + 999.998081344856}$$

```python
pz.plot_3d_laplce()
```

```python
pz.plot_nyquist_with_pz()
```

Where now looking at the Bode and Nichols charts generated from the .pz analysis we see they are almost identical to what was gotten to the .ac analysis.

```python
pz.plot_bode_from_pz()
```

```python
pz.plot_nichols_from_pz()
```

# Add the Razavi T-coil after the previous magnetic section is done¶

# Citations

[1] T. Kuphardt, Lessons In Industrial Instrumentation :, Version 2.30. [online], 2018, Chapter 5 Section "Transfer Function Analysis". Available: https://control.com/textbook/ac-electricity/transfer-function-analysis/. [Accessed: 10- Jan- 2021].


```python
import numpy as np
%matplotlib tk
import matplotlib.pyplot as plt
import pickle
from sklearn import cluster
from sklearn import metrics

from sympy.solvers import solve
import sympy as sym

from scipy import optimize
```

```python
class VelocityPlotter():
    def __init__(self):
        personNames = ['person1','person2','person3', 'person4','person5','person6']
        colors = ['red', 'green', 'orange', 'cyan', 'magenta', 'black']

        picklesToLoad = ['person1.pickle', 'person2.pickle','person3.pickle', 'person4.pickle', 'person5.pickle', 'person6.pickle']
        startTimes = [[26.8, 382, 483.3], #person1
            [3.6, 352, 446.75], # person2
            [9.5, 378, 481.25], # person3
            [35.1, 436, 543], #person4
            [10.8, 387, 500], # person5
            [12.1, 364, 458.25], #person6
            ]

        self.x, self.y = sym.symbols('x y')
        self.k1, self.k2 = sym.symbols('k1 k2')

        self.eq_y = None
        self.eq_x = None

        dataSets = []
        for pickleToLoad in picklesToLoad:
            data = pickle.load(open(pickleToLoad, 'rb'))
            dataSets.append(data)

        newDataset = [[[] for _ in range(4)] for _ in range(len(dataSets))]
        for i, dataSet in enumerate(dataSets):
            x_log, distance_to_spot_log, time_log_angle, time_log_filter, spot_v_log, time_log_spot = dataSet
            for data in zip(x_log, distance_to_spot_log, time_log_angle, time_log_filter):
                if startTimes[i][1] > data[3] > startTimes[i][0]:
                    newDataset[i][0].append(data[0])
                    newDataset[i][1].append(data[1])
                    newDataset[i][2].append(data[2])
                    newDataset[i][3].append(data[3])
        self.all_distance = []
        self.all_vel = []
        self.all_angles = []

        fig2, ax2 = plt.subplots(1)

        for j, dataSet in enumerate(newDataset):
            x_log, distance_to_spot_log, time_log_angle, time_log_filter = dataSet

            #fig, ax = plt.subplots(1)
            for i in range(len(time_log_angle)):
                time_log_angle[i] = (time_log_angle[i]-np.pi) * (180/np.pi)

            person_velocity = []
            for i in range(len(x_log)):
                person_velocity.append( np.linalg.norm( np.array(x_log[i])[3:5] ) )

            bool_array = np.array(person_velocity) > 0.25
            person_velocity = list(np.array(person_velocity)[bool_array == True])
            distance_to_spot_log = list(np.array(distance_to_spot_log)[bool_array == True])
            time_log_angle = list(np.array(time_log_angle)[bool_array == True])
            time_log_filter = list(np.array(time_log_filter)[bool_array == True])

            #print(len(distance_to_spot_log))
            #print(len(time_log_angle))
            #print(len(time_log_filter))

            self.all_distance = self.all_distance + distance_to_spot_log
            self.all_vel = self.all_vel  + person_velocity
            self.all_angles = self.all_angles + time_log_angle

            filter = np.array(distance_to_spot_log) < 3.5
            coefs = np.polyfit(np.array(person_velocity)[filter == True], np.array(distance_to_spot_log)[filter == True], 1)

            # X = np.arange(0.1, 1.4, 0.1)
            # ax.plot(X, np.polyval(coefs, X), color="black")
            # ax.plot(np.array(person_velocity)[filter == True], np.array(distance_to_spot_log)[filter == True], 'o', label=personNames[j], color=colors[j])
            # ax.set_ylabel('Distance to Spot [m]')
            # ax.set_xlabel('Speed [m/s]')
            # ax.legend()
            # ax.set_ylim([0.5, 3.5])

            ax2.plot(np.array(person_velocity)[filter == True], np.array(distance_to_spot_log)[filter == True], 'o', markersize=1, label=personNames[j], color=colors[j])
            ax2.set_ylabel('Distance to Spot [m]')
            ax2.set_xlabel('Speed [m/s]')
            ax2.legend()
            ax2.set_ylim([0.5, 3.5])
            ax2.set_xlim([0.1, 1.5])
            #print("dist:" ,personNames[j], np.median(distance_to_spot_log))
            #print("angle:" ,personNames[j], np.median(time_log_angle))

        #fig, ax3 = plt.subplots(1)
        self.X = np.arange(-2, 5, 0.5)
        self.Y = np.arange(0, 10, 0.5)

        filter = np.array(self.all_distance) < 3.5
        self.coefs = np.polyfit(np.array(self.all_vel)[filter == True], np.array(self.all_distance)[filter == True], 1)
        #self.coefs = np.array([1, 0])

        self.filtered = (np.array(self.all_vel)[filter == True], np.array(self.all_distance)[filter == True])
        ax2.plot(self.X, np.polyval(self.coefs, self.X), color="black")

        fig2, ax5 = plt.subplots(1)

        def point_on_curve(x0, y0):
            x_min = sym.Symbol('x')
            mean = np.polyval(self.coefs, x_min)
            the_diff = sym.diff( sym.sqrt( (x_min - x0)**2 + (mean - y0)**2 ), x_min )
            return_var = solve(the_diff, x_min)
            return float(sym.re(return_var[0])), float(sym.re(np.polyval(self.coefs, return_var[0])))

        def equation(x):
            x_min, y_min = point_on_curve(x[0], x[1])

            distance = sym.sqrt( (x_min - x[0])**2 + (y_min - x[1])**2 )#

            x1 = 1.4
            y1 = np.polyval(self.coefs, x1)

            arc_length = sym.sqrt((x1 - x_min)**2 + (y1 - y_min)**2)
            return float(sym.re(distance + arc_length))

        def func3d(x0, y0):
            x2, y2 = point_on_curve(x0, y0)
            dist = sym.sqrt( (x2 - x0)**2 + (y2 - y0)**2 )

            x1 = 1.4
            y1 = np.polyval(self.coefs, x1)

            arc_length = np.linalg.norm([x1 - x2,
                                        y1 - y2])

            if float(sym.re(dist + arc_length)) < 0.2:
                return 0.2

            return float(sym.re(dist + arc_length))

        def point_on_line(x0, y0):
            a,b = self.coefs[0], self.coefs[1]

            y = y0 - b
            vector = np.array([1, a])
            the_length = np.dot(np.array([x0, y]), vector) / np.dot(vector, vector)
            point = (the_length * vector)
            return point[0], point[1] + b

        def func3dLinear(x0, y0):
            x1, y1 = point_on_line(x0, y0)

            k1 = sym.symbols('kp1')
            k2 = sym.symbols('kp2')

            #return sym.sqrt((x0500 - x1)**2 + (y0 - y1)**2)
            return k1*sym.sqrt( (1.4 - x1)**2 + (np.polyval(self.coefs, 1.4) - y1)**2 )**2 + k2*sym.sqrt((x0 - x1)**2 + (y0 - y1)**2)**2

        def quiver(x0, y0):
            if self.eq_x == None:
                eq = func3dLinear(self.x, self.y)
                self.eq_x = eq.diff(self.x)
                self.eq_y = eq.diff(self.y)

            return (float(self.eq_x.subs(self.x, x0).subs(self.y, y0).subs(self.k1, 1).subs(self.k2, 1)),
             float(self.eq_y.subs(self.x, x0).subs(self.y, y0).subs(self.k1, 1).subs(self.k2, 1)))

        self.grads = np.zeros((len(self.X), len(self.Y), 2))
        for ind_x, x in enumerate(self.X):
            for ind_y, y in enumerate(self.Y):
                values = quiver(x, y)
                self.grads[ind_x, ind_y, :] = np.asfarray(values)
                ax5.quiver(x, y, -values[0], -values[1])

        #func3d_vectorized = np.vectorize(func3dLinear)
        #self.Z = np.zeros((len(self.Y),len(self.X)))
        #for x_ind, x_val in enumerate(self.X):
        #    for y_ind, y_val in enumerate(self.Y):
        #        self.Z[y_ind, x_ind] = func3dLinear(x_val, y_val)

        #X, Y = np.meshgrid(self.X, self.Y)
        #self.Z = np.asfarray(func3d_vectorized(Y, X))

letsgoo = VelocityPlotter()
```

```python
fig, ax3 = plt.subplots(1)
cm = plt.cm.get_cmap('viridis')
#ax3.contourf(letsgoo.X, letsgoo.Y, letsgoo.Z, 50)
#ax3.plot(letsgoo.X, np.polyval(letsgoo.coefs, letsgoo.X), 'r-')
#ax3.set_xlim([0,5])
#ax3.set_ylim([0,5])
#letsgoo.coefs

print([sym.simplify(letsgoo.eq_x), sym.simplify(letsgoo.eq_y)])
```

```python
fig, ax3 = plt.subplots(1)
cm = plt.cm.get_cmap('viridis')
ax3.plot(letsgoo.X, np.polyval(letsgoo.coefs, letsgoo.X), 'r-')

x1, y1 = 1, 2

ax3.plot(x1, y1, 'ro')

def point_on_line(x0, y0):
    a,b = letsgoo.coefs[0], letsgoo.coefs[1]
    y = y0 - b
    vector = np.array([1, a])
    the_length = np.dot(np.array([x0, y]), np.transpose(vector)) / np.dot(np.transpose(vector), vector)
    point = (the_length * vector) + np.array([0, b])
    return point

ax3.plot(point_on_line(x1,y1)[0], point_on_line(x1,y1)[1], 'ro')

print(sym.sqrt( (1.4 - point_on_line(x1,y1)[0])**2 + (np.polyval(letsgoo.coefs, 1.4) - point_on_line(x1,y1)[1])**2 ))

```

    0.525930084844368

```python
fig, ax3 = plt.subplots(1)
cm = plt.cm.get_cmap('viridis')
ax3.contourf(letsgoo.X, letsgoo.Y, letsgoo.Z)

x_i = 1
y_i = 1
Last_cost = letsgoo.Z[y_i, x_i]
iteration = 0
while True: #not (1.4 + 0.01 > letsgoo.X[x_i] > 1.4 - 0.01):
    iteration += 1
    if iteration > 50:
        break
    new_x = x_i
    new_y = y_i
    for x in range(-1, 2):
        for y in range(-1, 2):
            if letsgoo.Z[y_i + y, x_i + x] < Last_cost:
                new_x = x_i + x
                new_y = y_i + y
                Last_cost = letsgoo.Z[y_i + y, x_i + x]

    x_i = new_x
    y_i = new_y

    ax3.plot(letsgoo.X[x_i], letsgoo.Y[y_i], 'o')
```

```python
vale = []
for i in range(1, 15):
    polynomial = np.polyfit(letsgoo.filtered[0], letsgoo.filtered[1], i, full=True)
    vale.append(polynomial[1] / len(letsgoo.filtered[0]))

    # r-squared
    p = np.poly1d(polynomial[0])
    # fit values, and mean
    yhat = p(letsgoo.filtered[0])                         # or [p(z) for z in x]
    ybar = np.sum(letsgoo.filtered[1])/len(letsgoo.filtered[1])          # or sum(y)/len(y)
    ssreg = np.sum((yhat-ybar)**2)   # or sum([ (yihat - ybar)**2 for yihat in yhat])
    sstot = np.sum((letsgoo.filtered[1] - ybar)**2)    # or sum([ (yi - ybar)**2 for yi in y])
    print("rSquared: ", ssreg/sstot)

vale
```

    rSquared:  0.35910237303077575
    rSquared:  0.3780233587026233
    rSquared:  0.4094850453367208
    rSquared:  0.4145768119160963
    rSquared:  0.415720663593014
    rSquared:  0.41595788434227726
    rSquared:  0.4164028486118921
    rSquared:  0.4164061203975021
    rSquared:  0.4214773482509907
    rSquared:  0.4230772684668947
    rSquared:  0.4230772699172622
    rSquared:  0.4259557357191395
    rSquared:  0.4275705445138496
    rSquared:  0.4407208059074988

    [array([0.12173258]),
     array([0.11813871]),
     array([0.11216285]),
     array([0.11119572]),
     array([0.11097846]),
     array([0.1109334]),
     array([0.11084888]),
     array([0.11084826]),
     array([0.10988503]),
     array([0.10958114]),
     array([0.10958114]),
     array([0.10903439]),
     array([0.10872767]),
     array([0.10622979])]

```python
plt.plot(range(1, len(vale) + 1),vale)
```

    [<matplotlib.lines.Line2D at 0x7f0f5f4d39a0>]

```python
fig = plt.figure()
ax3 = fig.gca(projection='3d')

X, Y = np.meshgrid(letsgoo.X, letsgoo.Y)
Z = letsgoo.Z

ax3.plot_surface(X, Y, Z, cmap='jet')

ax3.set_ylabel('Distance to spot [m]')
ax3.set_xlabel('Speed [m/s]')
ax3.set_zlabel('Cost')
```

    /tmp/ipykernel_18162/571970758.py:2: MatplotlibDeprecationWarning: Calling gca() with keyword arguments was deprecated in Matplotlib 3.4. Starting two minor releases later, gca() will take no keyword arguments. The gca() function should only be used to get the current axes, or if no axes exist, create new axes with default keyword arguments. To create a new axes with non-default arguments, use plt.axes() or plt.subplot().
      ax3 = fig.gca(projection='3d')

    Text(0.5, 0, 'Cost')

```python
import pickle

pickle.dump((letsgoo.X, letsgoo.Y, letsgoo.grads), open("gradients.pickle", "wb"))
```

```python
fig2, ax5 = plt.subplots(1)
X, Y, grads = pickle.load(open("gradients.pickle", 'rb'))
for ind_x, x in enumerate(X):
    for ind_y, y in enumerate(Y):
        ax5.quiver(x, y, -grads[ind_x, ind_y, 0], -grads[ind_x, ind_y, 1])
```


     Histograms of Oriented Gradients (HOG)

Introduction

As we saw with the ORB algorithm, we can use keypoints in images to do keypoint-based matching to detect objects in images. These type of algorithms work great when you want to detect objects that have a lot of consistent internal features that are not affected by the background. For example, these algorithms work well for facial detection because faces have a lot of consistent internal features that don’t get affected by the image background, such as the eyes, nose, and mouth. However, these type of algorithms don’t work so well when attempting to do more general object recognition, say for example, pedestrian detection in images. The reason is that people don’t have consistent internal features, like faces do, because the body shape and style of every person is different (see Fig. 1). This means that every person is going to have a different set of internal features, and so we need something that can more generally describe a person.

Fig. 1. - Pedestrians.

One option is to try to detect pedestrians by their contours instead.  Detecting objects in images by their contours (boundaries) is very challenging because we have to deal with the difficulties brought about by the contrast between the background and the foreground. For example, suppose you wanted to detect a pedestrian in an image that is walking in front of a white building and she is wearing a white coat and black pants (see Fig. 2). We can see in Fig. 2, that since the background of the image is mostly white, the black pants are going to have a very high contrast, but the coat, since it is white as well, is going to have very low contrast. In this case, detecting the edges of pants is going to be easy but detecting the edges of the coat is going to be very difficult. This is where **HOG** comes in. HOG stands for **Histograms of Oriented Gradients** and it was first introduced by Navneet Dalal and Bill Triggs in 2005.

Fig. 2. - High and Low Contrast.

The HOG algorithm works by creating histograms of the distribution of gradient orientations in an image and then normalizing them in a very special way. This special normalization is what makes HOG so effective at detecting the edges of objects even in cases where the contrast is very low. These normalized histograms are put together into a feature vector, known as the HOG descriptor, that can be used to train a machine learning algorithm, such as a Support Vector Machine (SVM), to detect objects in images based on their boundaries (edges). Due to its great success and reliability, HOG has become one of the most widely used algorithms in computer vison for object detection.

In this notebook, you will learn:

* How the HOG algorithm works
* How to use OpenCV to create a HOG descriptor
* How to visualize the HOG descriptor.

# The HOG Algorithm

As its name suggests, the HOG algorithm, is based on creating histograms from the orientation of image gradients. The HOG algorithm is implemented in a series of steps:

1. Given the image of particular object, set a detection window (region of interest) that covers the entire object in the image (see Fig. 3).

2. Calculate the magnitude and direction of the gradient for each individual pixel in the detection window.

3. Divide the detection window into connected *cells* of pixels, with all cells being of the same size (see Fig. 3). The size of the cells is a free parameter and it is usually chosen so as to match the scale of the features that want to be detected. For example, in a 64 x 128 pixel detection window, square cells 6 to 8 pixels wide are suitable for detecting human limbs.

4. Create a Histogram for each cell, by first grouping the gradient directions of all pixels in each cell into a particular number of orientation (angular) bins; and then adding up the gradient magnitudes of the gradients in each angular bin (see Fig. 3). The number of bins in the histogram is a free parameter and it is usually set to 9 angular bins.

5. Group adjacent cells into *blocks* (see Fig. 3). The number of cells in each block is a free parameter and all blocks must be of the same size. The distance between each block (known as the stride) is a free parameter but it is usually set to half the block size, in which case you will get overlapping blocks (*see video below*).  The HOG algorithm has been shown empirically to work better with overlapping blocks.

6. Use the cells contained within each block to normalize the cell histograms in that block (see Fig. 3). If you have overlapping blocks this means that most cells will be normalized with respect to different blocks (*see video below*). Therefore, the same cell may have several different normalizations.

7. Collect all the normalized histograms from all the blocks into a single feature vector called the HOG descriptor.

8. Use the resulting HOG descriptors from many images of the same type of object to train a machine learning algorithm, such as an SVM, to detect those type of objects in images. For example, you could use the HOG descriptors from many images of pedestrians to train an SVM to detect pedestrians in images. The training is done with both positive a negative examples of the object you want detect in the image.

9. Once the SVM has been trained, a sliding window approach is used to try to detect and locate objects in images.  Detecting an object in the image entails finding the part of the image that looks similar to the HOG pattern learned by the SVM.

Fig. 3. - HOG Diagram.

Vid. 1. - HOG Animation.

# Why The HOG Algorithm Works

As we learned above, HOG creates histograms by adding the magnitude of the gradients in particular orientations in localized portions of the image called *cells*.  By doing this we guarantee that stronger gradients will contribute more to the magnitude of their respective angular bin, while the effects of weak and randomly oriented gradients resulting from noise are minimized. In this manner the histograms tell us the dominant gradient orientation of each cell.

### Dealing with contrast

Now, the magnitude of the dominant orientation can vary widely due to variations in local illumination and the contrast between the background and the foreground.

To account for the background-foreground contrast differences, the HOG algorithm tries to detect edges locally.  In order to do this, it defines groups of cells, called **blocks**, and normalizes the histograms using this local group of cells. By normalizing locally, the HOG algorithm can detect the edges in each block very reliably; this is called **block normalization**.

In addition to using block normalization, the HOG algorithm also uses overlapping blocks to increase its performance. By using overlapping blocks, each cell contributes several independent components to the final HOG descriptor, where each component corresponds to a cell being normalized with respect to a different block. This may seem redundant but, it has been shown empirically that by normalizing each cell several times with respect to different local blocks, the performance of the HOG algorithm increases dramatically.

### Loading Images and Importing Resources

The first step in building our HOG descriptor is to load the required packages into Python and to load our image.

We start by using OpenCV to load an image of a triangle tile. Since, the `cv2.imread()` function loads images as BGR we will convert our image to RGB so we can display it with the correct colors. As usual we will convert our BGR image to Gray Scale for analysis.

```python
import cv2
import numpy as np
import matplotlib.pyplot as plt

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Load the image
image = cv2.imread('./images/triangle_tile.jpeg')

# Convert the original image to RGB
original_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# Convert the original image to gray scale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Print the shape of the original and gray scale images
print('The original image has shape: ', original_image.shape)
print('The gray scale image has shape: ', gray_image.shape)

# Display the images
plt.subplot(121)
plt.imshow(original_image)
plt.title('Original Image')
plt.subplot(122)
plt.imshow(gray_image, cmap='gray')
plt.title('Gray Scale Image')
plt.show()
```

    The original image has shape:  (250, 250, 3)
    The gray scale image has shape:  (250, 250)

    <matplotlib.figure.Figure at 0x7f60645def60>

# Creating The HOG Descriptor

We will be using OpenCV’s `HOGDescriptor` class to create the HOG descriptor. The parameters of the HOG descriptor are setup using the `HOGDescriptor()` function. The parameters of the `HOGDescriptor()` function and their default values are given below:

`cv2.HOGDescriptor(win_size = (64, 128),
                  block_size = (16, 16),
                  block_stride = (8, 8),
                  cell_size = (8, 8),
                  nbins = 9,
                  win_sigma = DEFAULT_WIN_SIGMA,
                  threshold_L2hys = 0.2,
                  gamma_correction = true,
                  nlevels = DEFAULT_NLEVELS)`

Parameters:

* **win_size** – *Size*
Size of detection window in pixels (*width, height*). Defines the region of interest. Must be an integer multiple of cell size.

* **block_size** – *Size*
Block size in pixels (*width, height*). Defines how many cells are in each block. Must be an integer multiple of cell size and it must be smaller than the detection window. The smaller the block the finer detail you will get.

* **block_stride** – *Size*
Block stride in pixels (*horizontal, vertical*). It must be an integer multiple of cell size. The `block_stride` defines the distance between adjecent blocks, for example, 8 pixels horizontally and 8 pixels vertically. Longer `block_strides` makes the algorithm run faster (because less blocks are evaluated) but the algorithm may not perform as well.

* **cell_size** – *Size*
Cell size in pixels (*width, height*). Determines the size fo your cell. The smaller the cell the finer detail you will get.

* **nbins** – *int*
Number of bins for the histograms. Determines the number of angular bins used to make the histograms. With more bins you capture more gradient directions. HOG uses unsigned gradients, so the angular bins will have values between 0 and 180 degrees.

* **win_sigma** – *double*
Gaussian smoothing window parameter. The performance of the HOG algorithm can be improved by smoothing the pixels near the edges of the blocks by applying a Gaussian spatial window to each pixel before computing the histograms.

* **threshold_L2hys** – *double*
L2-Hys (Lowe-style clipped L2 norm) normalization method shrinkage. The L2-Hys method is used to normalize the blocks and it consists of an L2-norm followed by clipping and a renormalization. The clipping limits the maximum value of the descriptor vector for each block to have the value of the given threshold (0.2 by default). After the clipping the descriptor vector is renormalized as described in *IJCV*, 60(2):91-110, 2004.

* **gamma_correction** – *bool*
Flag to specify whether the gamma correction preprocessing is required or not. Performing gamma correction slightly increases the performance of the HOG algorithm.

* **nlevels** – *int*
Maximum number of detection window increases.

As we can see, the  `cv2.HOGDescriptor()`function supports a wide range of parameters. The first few arguments (`block_size, block_stride, cell_size`, and `nbins`) are probably the ones you are most likely to change. The other parameters can be safely left at their default values and you will get good results.

In the code below, we will use the `cv2.HOGDescriptor()`function to set the cell size, block size, block stride, and the number of bins for the histograms of the HOG descriptor. We will then use `.compute(image)`method to compute the HOG descriptor (feature vector) for the given `image`.

```python
# Specify the parameters for our HOG descriptor

# Cell Size in pixels (width, height). Must be smaller than the size of the detection window
# and must be chosen so that the resulting Block Size is smaller than the detection window.
cell_size = (6, 6)

# Number of cells per block in each direction (x, y). Must be chosen so that the resulting
# Block Size is smaller than the detection window
num_cells_per_block = (2, 2)

# Block Size in pixels (width, height). Must be an integer multiple of Cell Size.
# The Block Size must be smaller than the detection window
block_size = (num_cells_per_block[0] * cell_size[0],
              num_cells_per_block[1] * cell_size[1])

# Calculate the number of cells that fit in our image in the x and y directions
x_cells = gray_image.shape[1] // cell_size[0]
y_cells = gray_image.shape[0] // cell_size[1]

# Horizontal distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (x_cells - num_cells_per_block[0]) / h_stride = integer.
# intenger is the number of movements of the block
h_stride = 1

# Vertical distance between blocks in units of Cell Size. Must be an integer and it must
# be set such that (y_cells - num_cells_per_block[1]) / v_stride = integer.
# intenger is the number of movements of the block
v_stride = 1

# Block Stride in pixels (horizantal, vertical). Must be an integer multiple of Cell Size
block_stride = (cell_size[0] * h_stride, cell_size[1] * v_stride)

# Number of gradient orientation bins
num_bins = 9

# Specify the size of the detection window (Region of Interest) in pixels (width, height).
# It must be an integer multiple of Cell Size and it must cover the entire image. Because
# the detection window must be an integer multiple of cell size, depending on the size of
# your cells, the resulting detection window might be slightly smaller than the image.
# This is perfectly ok.
win_size = (x_cells * cell_size[0] , y_cells * cell_size[1])

# Print the shape of the gray scale image for reference
print('\nThe gray scale image has shape: ', gray_image.shape)
print()

# Print the parameters of our HOG descriptor
print('HOG Descriptor Parameters:\n')
print('Window Size:', win_size)
print('Cell Size:', cell_size)
print('Block Size:', block_size)
print('Block Stride:', block_stride)
print('Number of Bins:', num_bins)
print()

# Set the parameters of the HOG descriptor using the variables defined above
hog = cv2.HOGDescriptor(win_size, block_size, block_stride, cell_size, num_bins)

# Compute the HOG Descriptor for the gray scale image
hog_descriptor = hog.compute(gray_image)
```

    The gray scale image has shape:  (250, 250)

    HOG Descriptor Parameters:

    Window Size: (246, 246)
    Cell Size: (6, 6)
    Block Size: (12, 12)
    Block Stride: (6, 6)
    Number of Bins: 9

# Number of Elements In The HOG Descriptor

The resulting HOG Descriptor (feature vector), contains the normalized histograms from all cells from all blocks in the detection window concatenated in one long vector. Therefore, the size of the HOG feature vector will be given by the total number of blocks in the detection window, multiplied by the number of cells per block, times the number of orientation bins:

\begin{equation}
\mbox{total_elements} = (\mbox{total_number_of_blocks})\mbox{ } \times \mbox{ } (\mbox{number_cells_per_block})\mbox{ } \times \mbox{ } (\mbox{number_of_bins})
\end{equation}

If we don’t have overlapping blocks (*i.e.* the `block_stride`equals the `block_size`), the total number of blocks can be easily calculated by dividing the size of the detection window by the block size. However, in the general case we have to take into account the fact that we have overlapping blocks. To find the total number of blocks in the general case (*i.e.* for any `block_stride` and `block_size`), we can use the formula given below:

\begin{equation}
\mbox{Total}_i = \left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right)\left( \frac{\mbox{window_size}_i}{\mbox{block_size}_i} \right) - \left [\left( \frac{\mbox{block_size}_i}{\mbox{block_stride}_i} \right) - 1 \right]; \mbox{  for  } i = x,y
\end{equation}

Where Total$_x$, is the total number of blocks along the width of the detection window, and Total$_y$, is the total number of blocks along the height of the detection window. This formula for Total$_x$ and Total$_y$, takes into account the extra blocks that result from overlapping. After calculating Total$_x$ and Total$_y$, we can get the total number of blocks in the detection window by multiplying Total$_x$  $\times$ Total$_y$. The above formula can be simplified considerably because the `block_size`, `block_stride`, and `window_size`are all defined in terms of the `cell_size`. By making all the appropriate substitutions and cancelations the above formula reduces to:

\begin{equation}
\mbox{Total}_i = \left(\frac{\mbox{cells}_i - \mbox{num_cells_per_block}_i}{N_i}\right) + 1\mbox{  }; \mbox{  for  } i = x,y
\end{equation}

Where cells$_x$ is the total number of cells along the width of the detection window, and cells$_y$, is the total number of cells along the height of the detection window. And $N_x$ is the horizontal block stride in units of `cell_size` and $N_y$ is the vertical block stride in units of `cell_size`.

Let's calculate what the number of elements for the HOG feature vector should be and check that it matches the shape of the HOG Descriptor calculated above.

```python
# Calculate the total number of blocks along the width of the detection window
tot_bx = np.uint32(((x_cells - num_cells_per_block[0]) / h_stride) + 1)

# Calculate the total number of blocks along the height of the detection window
tot_by = np.uint32(((y_cells - num_cells_per_block[1]) / v_stride) + 1)

# Calculate the total number of elements in the feature vector
tot_els = (tot_bx) * (tot_by) * num_cells_per_block[0] * num_cells_per_block[1] * num_bins

# Print the total number of elements the HOG feature vector should have
print('\nThe total number of elements in the HOG Feature Vector should be: ',
      tot_bx, 'x',
      tot_by, 'x',
      num_cells_per_block[0], 'x',
      num_cells_per_block[1], 'x',
      num_bins, '=',
      tot_els)

# Print the shape of the HOG Descriptor to see that it matches the above
print('\nThe HOG Descriptor has shape:', hog_descriptor.shape)
print()
```

    The total number of elements in the HOG Feature Vector should be:  40 x 40 x 2 x 2 x 9 = 57600

    The HOG Descriptor has shape: (57600, 1)

# Visualizing The HOG Descriptor

We can visualize the HOG Descriptor by plotting the histogram associated with each cell as a collection of vectors. To do this, we will plot each bin in the histogram as a single vector whose magnitude is given by the height of the bin and its orientation is given by the angular bin that its associated with. Since any given cell might have multiple histograms associated with it, due to the overlapping blocks, we will choose to average all the histograms for each cell to produce a single histogram for each cell.

OpenCV has no easy way to visualize the HOG Descriptor, so we have to do some manipulation first in order to visualize it. We will start by reshaping the HOG Descriptor in order to make our calculations easier.  We will then compute the average histogram of each cell and finally we will convert the histogram bins into vectors. Once we have the vectors, we plot the corresponding vectors for each cell in an image.

The code below produces an interactive plot so that you can interact with the figure. The figure contains:
* the grayscale image,
* the HOG Descriptor (feature vector),
* a zoomed-in portion of the HOG Descriptor, and
* the histogram of the selected cell.

**You can click anywhere on the gray scale image or the HOG Descriptor image to select a particular cell**. Once you click on either image a *magenta* rectangle will appear showing the cell you selected. The Zoom Window will show you a zoomed in version of the HOG descriptor around the selected cell; and the histogram plot will show you the corresponding histogram for the selected cell. The interactive window also has buttons at the bottom that allow for other functionality, such as panning, and giving you the option to save the figure if desired. The home button returns the figure to its default value.

**NOTE**: If you are running this notebook in the Udacity workspace, there is around a 2 second lag in the interactive plot. This means that if you click in the image to zoom in, it will take about 2 seconds for the plot to refresh.

```python
%matplotlib notebook

import copy
import matplotlib.patches as patches

# Set the default figure size
plt.rcParams['figure.figsize'] = [9.8, 9]

# Reshape the feature vector to [blocks_y, blocks_x, num_cells_per_block_x, num_cells_per_block_y, num_bins].
# The blocks_x and blocks_y will be transposed so that the first index (blocks_y) referes to the row number
# and the second index to the column number. This will be useful later when we plot the feature vector, so
# that the feature vector indexing matches the image indexing.
hog_descriptor_reshaped = hog_descriptor.reshape(tot_bx,
                                                 tot_by,
                                                 num_cells_per_block[0],
                                                 num_cells_per_block[1],
                                                 num_bins).transpose((1, 0, 2, 3, 4))

# Print the shape of the feature vector for reference
print('The feature vector has shape:', hog_descriptor.shape)

# Print the reshaped feature vector
print('The reshaped feature vector has shape:', hog_descriptor_reshaped.shape)

# Create an array that will hold the average gradients for each cell
ave_grad = np.zeros((y_cells, x_cells, num_bins))

# Print the shape of the ave_grad array for reference
print('The average gradient array has shape: ', ave_grad.shape)

# Create an array that will count the number of histograms per cell
hist_counter = np.zeros((y_cells, x_cells, 1))

# Add up all the histograms for each cell and count the number of histograms per cell
for i in range (num_cells_per_block[0]):
    for j in range(num_cells_per_block[1]):
        ave_grad[i:tot_by + i,
                 j:tot_bx + j] += hog_descriptor_reshaped[:, :, i, j, :]

        hist_counter[i:tot_by + i,
                     j:tot_bx + j] += 1

# Calculate the average gradient for each cell
ave_grad /= hist_counter

# Calculate the total number of vectors we have in all the cells.
len_vecs = ave_grad.shape[0] * ave_grad.shape[1] * ave_grad.shape[2]

# Create an array that has num_bins equally spaced between 0 and 180 degress in radians.
deg = np.linspace(0, np.pi, num_bins, endpoint = False)

# Each cell will have a histogram with num_bins. For each cell, plot each bin as a vector (with its magnitude
# equal to the height of the bin in the histogram, and its angle corresponding to the bin in the histogram).
# To do this, create rank 1 arrays that will hold the (x,y)-coordinate of all the vectors in all the cells in the
# image. Also, create the rank 1 arrays that will hold all the (U,V)-components of all the vectors in all the
# cells in the image. Create the arrays that will hold all the vector positons and components.
U = np.zeros((len_vecs))
V = np.zeros((len_vecs))
X = np.zeros((len_vecs))
Y = np.zeros((len_vecs))

# Set the counter to zero
counter = 0

# Use the cosine and sine functions to calculate the vector components (U,V) from their maginitudes. Remember the
# cosine and sine functions take angles in radians. Calculate the vector positions and magnitudes from the
# average gradient array
for i in range(ave_grad.shape[0]):
    for j in range(ave_grad.shape[1]):
        for k in range(ave_grad.shape[2]):
            U[counter] = ave_grad[i,j,k] * np.cos(deg[k])
            V[counter] = ave_grad[i,j,k] * np.sin(deg[k])

            X[counter] = (cell_size[0] / 2) + (cell_size[0] * i)
            Y[counter] = (cell_size[1] / 2) + (cell_size[1] * j)

            counter = counter + 1

# Create the bins in degress to plot our histogram.
angle_axis = np.linspace(0, 180, num_bins, endpoint = False)
angle_axis += ((angle_axis[1] - angle_axis[0]) / 2)

# Create a figure with 4 subplots arranged in 2 x 2
fig, ((a,b),(c,d)) = plt.subplots(2,2)

# Set the title of each subplot
a.set(title = 'Gray Scale Image\n(Click to Zoom)')
b.set(title = 'HOG Descriptor\n(Click to Zoom)')
c.set(title = 'Zoom Window', xlim = (0, 18), ylim = (0, 18), autoscale_on = False)
d.set(title = 'Histogram of Gradients')

# Plot the gray scale image
a.imshow(gray_image, cmap = 'gray')
a.set_aspect(aspect = 1)

# Plot the feature vector (HOG Descriptor)
b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
b.invert_yaxis()
b.set_aspect(aspect = 1)
b.set_facecolor('black')

# Define function for interactive zoom
def onpress(event):

    #Unless the left mouse button is pressed do nothing
    if event.button != 1:
        return

    # Only accept clicks for subplots a and b
    if event.inaxes in [a, b]:

        # Get mouse click coordinates
        x, y = event.xdata, event.ydata

        # Select the cell closest to the mouse click coordinates
        cell_num_x = np.uint32(x / cell_size[0])
        cell_num_y = np.uint32(y / cell_size[1])

        # Set the edge coordinates of the rectangle patch
        edgex = x - (x % cell_size[0])
        edgey = y - (y % cell_size[1])

        # Create a rectangle patch that matches the the cell selected above
        rect = patches.Rectangle((edgex, edgey),
                                  cell_size[0], cell_size[1],
                                  linewidth = 1,
                                  edgecolor = 'magenta',
                                  facecolor='none')

        # A single patch can only be used in a single plot. Create copies
        # of the patch to use in the other subplots
        rect2 = copy.copy(rect)
        rect3 = copy.copy(rect)

        # Update all subplots
        a.clear()
        a.set(title = 'Gray Scale Image\n(Click to Zoom)')
        a.imshow(gray_image, cmap = 'gray')
        a.set_aspect(aspect = 1)
        a.add_patch(rect)

        b.clear()
        b.set(title = 'HOG Descriptor\n(Click to Zoom)')
        b.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 5)
        b.invert_yaxis()
        b.set_aspect(aspect = 1)
        b.set_facecolor('black')
        b.add_patch(rect2)

        c.clear()
        c.set(title = 'Zoom Window')
        c.quiver(Y, X, U, V, color = 'white', headwidth = 0, headlength = 0, scale_units = 'inches', scale = 1)
        c.set_xlim(edgex - cell_size[0], edgex + (2 * cell_size[0]))
        c.set_ylim(edgey - cell_size[1], edgey + (2 * cell_size[1]))
        c.invert_yaxis()
        c.set_aspect(aspect = 1)
        c.set_facecolor('black')
        c.add_patch(rect3)

        d.clear()
        d.set(title = 'Histogram of Gradients')
        d.grid()
        d.set_xlim(0, 180)
        d.set_xticks(angle_axis)
        d.set_xlabel('Angle')
        d.bar(angle_axis,
              ave_grad[cell_num_y, cell_num_x, :],
              180 // num_bins,
              align = 'center',
              alpha = 0.5,
              linewidth = 1.2,
              edgecolor = 'k')

        fig.canvas.draw()

# Create a connection between the figure and the mouse click
fig.canvas.mpl_connect('button_press_event', onpress)
plt.show()
```

    The feature vector has shape: (57600, 1)
    The reshaped feature vector has shape: (40, 40, 2, 2, 9)
    The average gradient array has shape:  (41, 41, 9)

    <IPython.core.display.Javascript object>

# Understanding The Histograms

Let's take a look at a couple of snapshots of the above figure to see if the histograms for the selected cell make sense. Let's start looking at a cell that is inside a triangle and not near an edge:

Fig. 4. - Histograms Inside a Triangle.

In this case, since the triangle is nearly all of the same color there shouldn't be any dominant gradient in the selected cell. As we can clearly see in the Zoom Window and the histogram, this is indeed the case. We have many gradients but none of them clearly dominates over the other.

Now let’s take a look at a cell that is near a horizontal edge:

Fig. 5. - Histograms Near a Horizontal Edge.

Remember that edges are areas of an image where the intensity changes abruptly. In these cases, we will have a high intensity gradient in some particular direction. This is exactly what we see in the corresponding histogram and Zoom Window for the selected cell.  In the Zoom Window, we can see that the dominant gradient is pointing up, almost at 90 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 90-degree bin in the histogram to dominate strongly over the others. This is in fact what we see.

Now let’s take a look at a cell that is near a vertical edge:

Fig. 6. - Histograms Near a Vertical Edge.

In this case we expect the dominant gradient in the cell to be horizontal, close to 180 degrees, since that’s the direction in which there is a sharp change in intensity. Therefore, we should expect to see the 170-degree bin in the histogram to dominate strongly over the others. This is what we see in the histogram but we also see that there is another dominant gradient in the cell, namely the one in the 10-degree bin. The reason for this, is because the HOG algorithm is using unsigned gradients, which means 0 degrees and 180 degrees are considered the same. Therefore, when the histograms are being created, angles between 160 and 180 degrees, contribute proportionally to both the 10-degree bin and the 170-degree bin. This results in there being two dominant gradients in the cell near the vertical edge instead of just one.

To conclude let’s take a look at a cell that is near a diagonal edge.

Fig. 7. - Histograms Near a Diagonal Edge.

To understand what we are seeing, let’s first remember that gradients have an *x*-component, and a *y*-component, just like vectors. Therefore, the resulting orientation of a gradient is going to be given by the vector sum of its components. For this reason, on vertical edges the gradients are horizontal, because they only have an x-component, as we saw in Figure 4. While on horizontal edges the gradients are vertical, because they only have a y-component, as we saw in Figure 3. Consequently, on diagonal edges, the gradients are also going to be diagonal because both the *x* and *y* components are non-zero.  Since the diagonal edges in the image are close to 45 degrees, we should expect to see a dominant gradient orientation in the 50-degree bin. This is in fact what we see in the histogram but, just like in Figure 4., we see there are two dominant gradients instead of just one. The reason for this is that when the histograms are being created, angles that are near the boundaries of bins, contribute proportionally to the adjacent bins.  For example, a gradient with an angle of 40 degrees, is right in the middle of the 30-degree and 50-degree bin. Therefore, the magnitude of the gradient is split evenly into the 30-degree and 50-degree bin. This results in there being two dominant gradients in the cell near the diagonal edge instead of just one.

Now that you know how HOG is implemented, in the workspace you will find a notebook named *Examples*. In there, you will be able set your own paramters for the HOG descriptor for various images. Have fun!

```python

```


# Welding schema

```python
%cd -q ..
```

```python
# some python imports that will be used throughout the tutorial
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import pandas as pd
import pint
import sympy
import xarray as xr
from mpl_toolkits.mplot3d import Axes3D

import asdf
```

```python
# importing the weldx package with prevalent default abbreviations
import weldx
import weldx.geometry as geo
import weldx.measurement as msm
import weldx.transformations as tf
import weldx.utility as ut
import weldx.visualization as vis
from weldx import Q_
from weldx.transformations import LocalCoordinateSystem as lcs
from weldx.transformations import WXRotation
from weldx.welding.groove.iso_9692_1 import get_groove
```

## Timestamp

```python
# file timestamp
reference_timestamp = pd.Timestamp("2020-11-09 12:00:00")
```

## Geometry

```python
# groove + trace = geometry
groove = get_groove(
    groove_type="VGroove",
    workpiece_thickness=Q_(5, "mm"),
    groove_angle=Q_(50, "deg"),
    root_face=Q_(1, "mm"),
    root_gap=Q_(1, "mm"),
)

# define the weld seam length in mm
seam_length = Q_(300, "mm")

# create a linear trace segment a the complete weld seam trace
trace_segment = geo.LinearHorizontalTraceSegment(seam_length)
trace = geo.Trace(trace_segment)

geometry = dict(groove_shape=groove, seam_length=seam_length)
```

## Setup the Coordinate System Manager (CSM)

```python
# crete a new coordinate system manager with default base coordinate system
csm = weldx.transformations.CoordinateSystemManager("base")

# add the workpiece coordinate system
csm.add_cs(
    coordinate_system_name="workpiece",
    reference_system_name="base",
    lcs=trace.coordinate_system,
)

tcp_start_point = Q_([5.0, 0.0, 2.0], "mm")
tcp_end_point = Q_([-5.0, 0.0, 2.0], "mm") + np.append(seam_length, Q_([0, 0], "mm"))

v_weld = Q_(10, "mm/s")
s_weld = (tcp_end_point - tcp_start_point)[0]  # length of the weld
t_weld = s_weld / v_weld

t_start = pd.Timedelta("0s")
t_end = pd.Timedelta(str(t_weld.to_base_units()))

rot = WXRotation.from_euler(seq="x", angles=180, degrees=True)

coords = [tcp_start_point.magnitude, tcp_end_point.magnitude]

tcp_wire = lcs(coordinates=coords, orientation=rot, time=[t_start, t_end])

# add the workpiece coordinate system
csm.add_cs(
    coordinate_system_name="tcp_wire",
    reference_system_name="workpiece",
    lcs=tcp_wire,
)

tcp_contact = lcs(coordinates=[0, 0, -10])

# add the workpiece coordinate system
csm.add_cs(
    coordinate_system_name="tcp_contact",
    reference_system_name="tcp_wire",
    lcs=tcp_contact,
)
```

## Measurements

```python
# time
time = pd.timedelta_range(start="0s", end="10s", freq="1ms")

# current data
I_ts = ut.sine(f=Q_(10, "1/s"), amp=Q_(20, "A"), bias=Q_(300, "A"))
I = I_ts.interp_time(time)
I["time"] = I["time"]

current_data = msm.Data(name="Welding current", data=I)

# voltage data
U_ts = ut.sine(f=Q_(10, "1/s"), amp=Q_(3, "V"), bias=Q_(40, "V"), phase=Q_(0.1, "rad"))
U = U_ts.interp_time(time)
U["time"] = U["time"]

voltage_data = msm.Data(name="Welding voltage", data=U)
```

```python
from asdf.tags.core import Software

HKS_sensor = msm.GenericEquipment(name="HKS P1000-S3")
BH_ELM = msm.GenericEquipment(name="Beckhoff ELM3002-0000")
twincat_scope = Software(name="Beckhoff TwinCAT ScopeView", version="3.4.3143")
```

```python
src_current = msm.Source(
    name="Current Sensor",
    output_signal=msm.Signal(signal_type="analog", unit="V", data=None),
    error=msm.Error(Q_(0.1, "percent")),
)

HKS_sensor.sources = []
HKS_sensor.sources.append(src_current)

from weldx.core import MathematicalExpression

[a, x, b] = sympy.symbols("a x b")
current_AD_func = MathematicalExpression(a * x + b)
current_AD_func.set_parameter("a", Q_(32768.0 / 10.0, "1/V"))
current_AD_func.set_parameter("b", Q_(0.0, ""))

current_AD_transform = msm.DataTransformation(
    name="AD conversion current measurement",
    input_signal=src_current.output_signal,
    output_signal=msm.Signal("digital", "", data=None),
    error=msm.Error(Q_(0.01, "percent")),
    func=current_AD_func,
)

BH_ELM.data_transformations = []
BH_ELM.data_transformations.append(current_AD_transform)

# define current output calibration expression and transformation
current_calib_func = MathematicalExpression(a * x + b)
current_calib_func.set_parameter("a", Q_(1000.0 / 32768.0, "A"))
current_calib_func.set_parameter("b", Q_(0.0, "A"))

current_calib_transform = msm.DataTransformation(
    name="Calibration current measurement",
    input_signal=current_AD_transform.output_signal,
    output_signal=msm.Signal("digital", "A", data=current_data),
    error=msm.Error(0.0),
    func=current_calib_func,
    meta=twincat_scope,
)

welding_current_chain = msm.MeasurementChain(
    name="welding current measurement chain",
    data_source=src_current,
    data_processors=[current_AD_transform, current_calib_transform],
)

welding_current = msm.Measurement(
    name="welding current measurement",
    data=[current_data],
    measurement_chain=welding_current_chain,
)
```

```python
src_voltage = msm.Source(
    name="Voltage Sensor",
    output_signal=msm.Signal("analog", "V", data=None),
    error=msm.Error(Q_(0.1, "percent")),
)

HKS_sensor.sources.append(src_voltage)

# define AD conversion expression and transformation step
[a, x, b] = sympy.symbols("a x b")
voltage_ad_func = MathematicalExpression(a * x + b)
voltage_ad_func.set_parameter("a", Q_(32768.0 / 10.0, "1/V"))
voltage_ad_func.set_parameter("b", Q_(0.0, ""))

voltage_AD_transform = msm.DataTransformation(
    name="AD conversion voltage measurement",
    input_signal=src_voltage.output_signal,
    output_signal=msm.Signal("digital", "", data=None),
    error=msm.Error(Q_(0.01, "percent")),
    func=voltage_ad_func,
)

HKS_sensor.data_transformations.append(voltage_AD_transform)

# define voltage output calibration expression and transformation
voltage_calib_func = MathematicalExpression(a * x + b)
voltage_calib_func.set_parameter("a", Q_(100.0 / 32768.0, "V"))
voltage_calib_func.set_parameter("b", Q_(0.0, "V"))

voltage_calib_transform = msm.DataTransformation(
    name="Calibration voltage measurement",
    input_signal=voltage_AD_transform.output_signal,
    output_signal=msm.Signal("digital", "V", data=voltage_data),
    error=msm.Error(0.0),
    func=voltage_calib_func,
    meta=twincat_scope,
)

welding_voltage_chain = msm.MeasurementChain(
    name="welding voltage measurement chain",
    data_source=src_voltage,
    data_processors=[voltage_AD_transform, voltage_calib_transform],
)

welding_voltage = msm.Measurement(
    name="welding voltage measurement",
    data=[voltage_data],
    measurement_chain=welding_voltage_chain,
)
```

## GMAW Process

```python
from weldx.welding.processes import GmawProcess
```

```python
params_pulse = dict(
    wire_feedrate=Q_(10.0, "m/min"),
    pulse_voltage=Q_(40.0, "V"),
    pulse_duration=Q_(5.0, "ms"),
    pulse_frequency=Q_(100.0, "Hz"),
    base_current=Q_(60.0, "A"),
)
process_pulse = GmawProcess(
    "pulse",
    "CLOOS",
    "Quinto",
    params_pulse,
    tag="CLOOS/pulse",
    meta={"modulation": "UI"},
)
```

```python
from weldx.asdf.tags.weldx.aws.process.gas_component import GasComponent
from weldx.asdf.tags.weldx.aws.process.shielding_gas_for_procedure import (
    ShieldingGasForProcedure,
)
from weldx.asdf.tags.weldx.aws.process.shielding_gas_type import ShieldingGasType
```

```python
gas_comp = [
    GasComponent("argon", Q_(82, "percent")),
    GasComponent("carbon dioxide", Q_(18, "percent")),
]
gas_type = ShieldingGasType(gas_component=gas_comp, common_name="SG")

gas_for_procedure = ShieldingGasForProcedure(
    use_torch_shielding_gas=True,
    torch_shielding_gas=gas_type,
    torch_shielding_gas_flowrate=Q_(20, "l / min"),
)
```

```python
process = dict(welding_process=process_pulse, shielding_gas=gas_for_procedure)
```

## ASDF file

```python
tree = dict(
    reference_timestamp=reference_timestamp,
    equipment=[HKS_sensor, BH_ELM],
    measurements=[welding_current, welding_voltage],
    welding_current=current_calib_transform.output_signal,
    welding_voltage=voltage_calib_transform.output_signal,
    coordinate_systems=csm,
    geometry=geometry,
    process=process,
    meta={"welder": "A.W. Elder"},
)

buffer = weldx.asdf.utils._write_buffer(
    tree,
    asdffile_kwargs=dict(
        custom_schema="./weldx/asdf/schemas/weldx.bam.de/weldx/datamodels/single_pass_weld-1.0.0.schema.yaml"
    ),
)
weldx.asdf.utils.notebook_fileprinter(buffer)
```

```python
filename = "schema_example_01.asdf"

with asdf.AsdfFile(
    tree,
    ignore_version_mismatch=False,
    custom_schema="./weldx/asdf/schemas/weldx.bam.de/weldx/datamodels/single_pass_weld-1.0.0.schema.yaml",
) as ff:
    ff.write_to(filename)
```

```python

```