```python
%matplotlib inline
# plots graphs within the notebook
%config InlineBackend.figure_format='svg' # not sure what this does, may be default images to svg format
from __future__ import division
from IPython.display import Image

from IPython.core.display import HTML
def header(text):
    raw_html = '<h4>' + str(text) + '</h4>'
    return raw_html

def box(text):
    raw_html = '<div style="border:1px dotted black;padding:2em;">'+str(text)+'</div>'
    return HTML(raw_html)

def nobox(text):
    raw_html = '<p>'+str(text)+'</p>'
    return HTML(raw_html)

def addContent(raw_html):
    global htmlContent
    htmlContent += raw_html
```

### Library for thermodynamic properties

Defines thermodynamic properties of air and water at 1 atm. Air properties are tabled between $-150\text{$^\circ$C}$ and $400\text{$^\circ$C}$, water between $274\text{K}$ and $373\text{K}$, Argon between $100\text{K}$ and $700\text{K}$ and Krypton between $150\text{K}$ and $750\text{K}$

Input must be in Kelvin

Use the scipy functions C2K  and F2K  to convert temperatures to Kelvin. Thermodynamic properties are linearly interpolated from the two nearest temperature states.

```python
%%file thermodynamics.py
""" Object name: Fluid"""
import numpy as np
import scipy
import scipy.optimize
from scipy.constants import convert_temperature
def C2K(T):
    return convert_temperature(T,'Celsius','Kelvin')
def C2F(T):
    return convert_temperature(T,'Celsius','Fahrenheit')
def F2K(T):
    return convert_temperature(T,'Fahrenheit','Kelvin')
def F2C(T):
    return convert_temperature(T,'Fahrenheit','Celsius')
def K2F(T):
    return convert_temperature(T,'Kelvin','Fahrenheit')
def K2C(T):
    return convert_temperature(T,'Kelvin','Celsius')
import scipy.constants as sc

def interpolate_table(target,index,xquantity,yquantity):
    return yquantity[index] + \
                (yquantity[index+1]-yquantity[index])* \
                (target-xquantity[index])/(xquantity[index+1]-xquantity[index])

class Fluid(object):
    """ How to:
        from NewLibraries import thermodynamics as thermo

        fluid_of_interest = thermo.Fluid(material,T) material can be air, water, argon and krypton (see below for ranges)
        and the temperature of the fluid T is in Kelvin.
        Outputs:
        The new object computes thermodynamic properties of air between -150 C and 400 C,
        water between 274K and 373K, argon between 100 and 700K and
        krypton between 150 and 700 K under 1 atm. Argon, krypton and water were obtained
        through http://webbook.nist.gov/chemistry/fluid/
        More fluids to be added in the future
        fluid_of_interest.beta thermal expansion coefficient
        fluid_of_interest.rho density
        fluid_of_interest.Cp specific heat
        fluid_of_interest.mu dynamic viscosity
        fluid_of_interest.k thermal conductivity
        fluid_of_interest.nu kinematic viscosity
        fluid_of_interest.alpha thermal diffusivity
        fluid_of_interest.Pr

        """
    def __init__(self,name,T,unit = "K",P = 101325.01):
        self.name = name
        if unit == "C":
            T = C2K(T)
        elif unit == "F":
            T = F2K(T)
        self.T = T
        self.P = P
        if P != 101325.01:
            print("All available tables are for P=1ATM, reverting to P=101325.01Pa")
            self.P = 101325.01
        if self.name == 'water':
            if T < 274 or T > 373:
                print("Temperature is out of bounds for liquid water")
                return
            Ttab,ptab,rhotab,Cptab,mutab,ktab = \
            np.genfromtxt('Tables/water1atm.csv', delimiter=',', skip_header = 1, unpack=True, dtype=float)
            Ntab = len(Ttab)
            Cptab *= 1e3
            nutab = mutab/rhotab
            alphatab = ktab/(rhotab*Cptab)
            Prtab = nutab/alphatab
            dTtab = Ttab[1] - Ttab[0]
            # compute beta from -rho(d rho/dT)
            betatab = -(1./rhotab)*np.gradient(rhotab)/dTtab
            i = int((T-Ttab[0])/dTtab)
            if (i == Ntab - 1):
                i == Ntab - 2
        elif self.name == 'argon':
            if T < 100 or T > 700:
                print("Temperature is out of bounds for argon")
                return
            Ttab,ptab,rhotab,Cptab,mutab,ktab = \
            np.loadtxt('Tables/Argon1atm.csv', delimiter=',', skiprows = 1, unpack=True, dtype=float)
            Ntab = len(Ttab)
            Cptab *= 1e3
            nutab = mutab/rhotab
            alphatab = ktab/(rhotab*Cptab)
            Prtab = nutab/alphatab
            dTtab = Ttab[1] - Ttab[0]
            # compute beta from -rho(d rho/dT)
            betatab = -(1./rhotab)*np.gradient(rhotab)/dTtab
            i = int((T-Ttab[0])/dTtab)
            if (i == Ntab - 1):
                i == Ntab - 2
        elif self.name == 'krypton':
            if T < 150 or T > 740:
                print("Temperature is out of bounds for krypton")
                return
            Ttab,ptab,rhotab,Cptab,mutab,ktab = \
            np.loadtxt('Tables/Krypton1atm.csv', delimiter=',', skiprows = 1, unpack=True, dtype=float)
            Ntab = len(Ttab)
            Cptab *= 1e3
            nutab = mutab/rhotab
            alphatab = ktab/(rhotab*Cptab)
            Prtab = nutab/alphatab
            dTtab = Ttab[1] - Ttab[0]
            # compute beta from -rho(d rho/dT)
            betatab = -(1./rhotab)*np.gradient(rhotab)/dTtab
            i = int((T-Ttab[0])/dTtab)
            if (i == Ntab - 1):
                i == Ntab - 2
        elif self.name == 'air':
            if T < C2K(-150.) or T > C2K(400.):
                print("Temperature is out of bounds of the table for air")
                return
            Ttab,rhotab,Cptab,ktab,nutab,betatab,Prtab = \
            np.genfromtxt('Tables/air1atm.csv', delimiter=',', skip_header = 1, unpack=True, dtype=float)
            Ntab = len(Ttab)
            Ttab = C2K(Ttab)
            Cptab *= 1e3
            nutab *= 1e-6
            mutab = rhotab*nutab
            alphatab = ktab/(rhotab*Cptab)
            Prtab = nutab/alphatab
            i = 0
            while (Ttab[i] < T) and (i<Ntab):
                i += 1
            i -=1
            if (i == Ntab - 1):
                i = Ntab - 2

        else:
            print("warning, no table available for", self.name)
            return

        self.rho = interpolate_table(T,i,Ttab,rhotab)
        self.Cp = interpolate_table(T,i,Ttab,Cptab)
        self.mu = interpolate_table(T,i,Ttab,mutab)
        self.k = interpolate_table(T,i,Ttab,ktab)
        self.nu = interpolate_table(T,i,Ttab,nutab)
        self.alpha = interpolate_table(T,i,Ttab,alphatab)
        self.Pr = interpolate_table(T,i,Ttab,Prtab)
        if (self.name == 'air'):
            self.beta = 1./T
        else:
            self.beta = interpolate_table(T,i,Ttab,betatab)

```

    Overwriting thermodynamics.py

```python
import thermodynamics as thermo
import numpy as np

fluid = thermo.Fluid('air',400.,"C")
print(fluid.name)

print(fluid.k)
```

    air
    0.0515

### Library of thermal resistances

```python
%%file HT_thermal_resistance.py
"""Object name: Resistance
   Function name: serial_sum(R,nori,nend), performs serial sum of a resistance object list from nori to nend
   Function name: parallel_sum(R,nori,nend), performs parallel sum of a resistance object list from nori to nend
   """
### definition of thermal resistance ###
from sympy.interactive import printing
printing.init_printing(use_latex='mathjax')

from IPython.display import display,Image, Latex
import numpy as np
import math
import scipy.constants as sc

import sympy as sym
#from sympy import *

class Resistance(object):
    """ Defines thermal resistances for conduction, convection and radiation heat transfer.
        First define the object attached with class with the name used in the thermal circuit
        and the units, which can only be 'W', 'W/m' or 'W/m^2'
        Second use self.conduction, self.convection or self.radiation to calculate your
        resistance. Each mode requires different arguments:

        from Libraries import HT_thermal_resistance as res
        R = []
        R.append(res.Resistance("$label$", "units")) where units = 'W', 'W/m' or 'W/m^2'

        then

        For conduction, there are 3 options:

        - R.cond_plane(k, L, A = 1.0) for planar conduction: k is the thermal conductivity,
                L is the thickness of the wall, and A is the optional surface area (=1 by default)
        - R.cond_cylinder(k , ra, rb, L = 1.0, angle = 2.*math.pi) for conduction in a
                cylindrical shell between the radii ra (internal) and rb (external). L is the length
                of the shell (optional, default = 1) and angle is angular dimension of shell, also
                optional and set to a full revolution by default (2 pi)
        - R.cond_sphere(k, ra, rb, scale = 1.0) for conductuion within a spherical shell bounded by radii ra and rb
            ra < rb. The optional parameter scale allows to calculate the thermal resistance for a fraction
            of a spherical shell. For instance a cornea is about 1/3 of spherical shell, so scale = 1./3.

        Convection:
        - R.convection(h, A = 1.0), where h is the convection coefficient (W/m^2K) and A is
        the surface area (optional, default is unit surface aera 1 m^2)

        Radiation:
        - R.radiation(eps, T_s, T_sur, A = 1.0), where eps is the permissivity of the material, T_s
        the surface temperature, T_sur the far away surface temperature, A the surface area (optional,
        by default A is the unit surface area 1 m^2).

        Contact:

        - R.contact(R,A,R_name= "R_{t}",A_name = "A",T_a_name = "T_a",Tb_name = "T_b"), where R is the contact resistance, typically obtained from a table
        A is the surface area
        The minimum number of arguments are:
        R.contact(R,A)

        R.display_equation(index) displays the heat flux/rate equations for a given resistance. index is the number of
        your resistance (you specify)

        Outputs:
        - R[i].R the resistance of element i, R[i].h the convection or radiation coefficient.

        Functions include
        R_tot = res.serial_sum(R,first_resistance,last_resistance) sums serial resistance
        R_tot = res.parallel_sum(R,first_resistance,last_resistance) sums parallel resistance

        """
    def __init__(self,name,units):
        self.name = name
        self.units = units
    def cond_plane(self, k, L, A = 1.0):
        self.mode = "conduction"
        self.geometry = "planar"
        self.k = k
        if k <= 0.:
            print("problem with the definition of thermal conductivity")
        self.L = L
        self.A = A
        self.R = self.L / (self.k * self.A)
    def cond_cylinder(self, k , ra, rb, L = 1.0, angle = 2.*math.pi):
        self.mode = "conduction"
        self.geometry = "cylindrical"
        self.k = k
        if k <= 0.:
            print("problem with the definition of thermal conductivity")
        self.ra = ra
        self.rb = rb
        if ra*rb <= 0.:
            print("problem with the definition of radii")
        self.L = L
        self.angle = angle
        self.R = np.log(rb/ra)/(angle*L*k)
    def cond_sphere(self, k, ra, rb, scale = 1.0):
        self.mode = "conduction"
        self.geometry = "spherical"
        self.k = k
        if k <= 0.:
            print("problem with the definition of thermal conductivity")
        self.ra = ra
        self.rb = rb
        if ra*rb <= 0.:
            print("problem with the definition of radii")
        self.R = (1./r_a-1./r_b)/(scale*4.*math.pi*k)
    def convection(self, h, A = 1.0):
        self.mode = 'convection'
        self.geometry = "whatever"
        self.R = 1./(h*A)
        self.A = A
        self.h = h
    def radiation(self,eps,T_s,T_sur, A = 1.0):
        self.R = 1./(eps*sc.sigma*(T_s+T_sur)*(T_s**2+T_sur**2)*A)
        self.mode = 'radiation'
        self.geometry = "whatever"
        self.A = A
        self.h = eps*sc.sigma*(T_s+T_sur)*(T_s**2+T_sur**2)
    def contact(self, R, A=1.0):
        self.R = R/A
        self.geometry = 'whatever'
        self.mode = 'contact'

### summation of thermal resistance (R is a vector) ###
def serial_sum(R,nori,nend):
    sum = 0.
    for i in range(nori,nend+1):
        sum += R[i].R
    return sum

def parallel_sum(R,nori,nend):
    sum = 0.
    for i in range(nori,nend+1):
        sum += 1./R[i].R
    return 1./sum

```

    Overwriting HT_thermal_resistance.py

```python
import HT_thermal_resistance as res
from HT_thermal_resistance import Resistance,serial_sum,parallel_sum

Rth = []
Rth.append(res.Resistance('$R_{cond,1}$','W/m^2'))
k = 0.1 #W.m^-1.K^-1
L = 0.05 #m
A =0.8*0.5

#Rth[0].conduction('plane',k,ra,rb,A,k_name,ra_name,rb_name,A_name,Ta_name,Tb_name)
Rth[0].cond_plane(k=k,L=L,A=A)
Rth.append(Resistance('$R_{conv,1}$','W/m^2'))
h = 10. #W.m^-2.K^-1

Rth[1].convection(h,A)
for i in range(2):
    print(i,Rth[i].name,Rth[i].mode,Rth[i].R)

#print(parallel_sum(Rth,0,1))

#R_total = serial_sum(Rth[:].R)
#print(R_total)
```

    0 $R_{cond,1}$ conduction 1.2499999999999998
    1 $R_{conv,1}$ convection 0.25

 Library for extended surfaces

```python
%%file HT_conduction_extended_surfaces.py
"""Object: ExtSurfaces"""
from sympy.interactive import printing
printing.init_printing(use_latex='mathjax')

from IPython.display import display,Image, Latex
import numpy as np
import math
import scipy.constants as sc

import sympy as sym
#from sympy import *

class ExtSurfaces(object):
    """ Defines temperature distribution, heat rate for constant cross sectional area fins.
        from Libraries import HT_conduction_extended_surfaces as condext

        fin = condext.ExtSurfaces(T_b,T_infty,T_L,k,h,P,Ac,L)
            calculates fin.m, fin.M which are constants used in flux calculation. Also provides
            fin.theta_b,.theta_L,.T_b,.T_infty,.T_L,.h,.k,.h,.P,.Ac,.L,.Af(fin exposed surface area)
        fin.heat_rate(bc) calculate the heat rate for bc="convection", "adiabatic", "isothermal", "infinite"
            The ouptuts are fin.q_f, fin.effectiveness, fin.resistance, fin.efficiency
        fin.temperature(bc,x) calculates the temperature as a function of bc and the location x
            The output is fin.theta_over_theta_b
        fin.equations(T_b_name,T_infty_name,T_L_name,k_name,h_name,P_name,Ac_name,L_name) writes all the equations for you
            you need to run fin.heat_rate first.
    """
    def __init__(self,T_b,T_infty,T_L,k,h,P,Ac,L):
        self.T_b = T_b
        self.T_infty = T_infty
        self.T_L = T_L
        theta_b = T_b-T_infty
        theta_L = T_L-T_infty
        self.theta_b = T_b-T_infty
        self.theta_L = T_L-T_infty
        self.k = k
        self.h = h
        self.P = P
        self.Ac = Ac
        self.L = L
        self.Af = self.P*self.L
        m = np.sqrt(self.h*self.P/(self.k*self.Ac))
        self.m = m
        M = np.sqrt(self.h*self.P*self.k*self.Ac)*self.theta_b
        self.M = M
    def heat_rate(self,bc):
        self.bc = bc
        it_works = True
        if self.bc == "convection":
            self.q_f = self.M*(np.sinh(self.m*self.L) + (self.h/(self.m*self.k))*np.cosh(self.m*self.L))/\
                    (np.cosh(self.m*self.L) + (self.h/(self.m*self.k))*np.sinh(self.m*self.L))
        elif self.bc == "adiabatic":
            self.q_f = self.M*np.tanh(self.m*self.L)
        elif self.bc == "isothermal":
            self.q_f = self.M*np.cosh(self.m*self.L - self.theta_L/self.theta_b)/np.sinh(self.m*self.L)
        elif self.bc == 'infinite':
            self.q_f = self.M
        else:
            print("boundary condition is not properly defined")
            it_works = False
        if it_works:
            self.effectiveness = self.q_f/(self.h*self.Ac*self.theta_b)
            self.Resistance = self.theta_b/self.q_f
            self.efficiency = self.q_f/(self.h*self.Af*self.theta_b)

    def temperature(self,bc,x):
        self.bc = bc
        if self.bc == "convection":
            self.theta_over_theta_b = (np.cosh(self.m*(self.L-x)) + (self.h/(self.m*self.k))*np.sinh(self.m*(self.L-x)))/\
                    (np.cosh(self.m*self.L) + (self.h/(self.m*self.k))*np.sinh(self.m*self.L))
        elif self.bc == "adiabatic":
            self.theta_over_theta_b = np.cosh(self.m*(self.L-x))/np.cosh(self.m*self.L)
        elif self.bc == "isothermal":
            self.theta_over_theta_b = ((self.theta_L/self.theta_b)*np.sinh(self.m*self.L)+np.sinh(self.m*self.L - x))\
                                        /np.sinh(self.m*self.L)
        elif self.bc == 'infinite':
            self.theta_over_theta_b = np.exp(-self.m*x)
        else:
            print("boundary condition is not properly defined")
        self.T_x = self.T_infty + self.theta_over_theta_b*self.theta_b

```

    Overwriting HT_conduction_extended_surfaces.py

```python
import  HT_conduction_extended_surfaces as extsurf
h = 100.
D = 0.005
P = np.pi*D
k = 398.
Ac = np.pi*(D**2)/4
T_b = 100.
T_infty = 25.
T_L = 0.
L = 0.04
fin = extsurf.ExtSurfaces(T_b,T_infty,T_L,k,h,P,Ac,L)
fin.heat_rate('isothermal')
print(fin.q_f)
fin.temperature('infinite',L/2.)
print(fin.theta_over_theta_b,fin.T_x)
```

    19.919974789199607
    0.7531035967174129 81.48276975380597

### Library of Nu correlations for external flows

```python
%%file HT_external_convection.py
""" Object name 1: FlatPlate
    Object name 2: CircularCylinder
    Object name 3: NoncircularCylinder
    Object name 4: BankofTubes
"""

from sympy.interactive import printing
printing.init_printing(use_latex='mathjax')

from IPython.display import display,Image, Latex
import numpy as np
import math
import scipy.constants as sc
import sys
try:
    import thermodynamics as thermo
except ModuleNotFoundError:
    from Libraries import thermodynamics as thermo
import sympy as sym
#from sympy import *

class FlatPlate(object):
    """ Definition of boundary layer thickness, friction coefficient, Nusselt number (both local and average)
        as a function of the regime.
        import HT_external_convection.py as extconv

        bl =extconv.FlatPlate(regime,thermal_bc,U_infty,nu,alpha,L,xi=0.0,Re_xc=5e5)
            where regime = 'laminar' or 'turbulent' or 'mixed',
            thermal_bc = 'isothermal', 'heat flux', 'unheated starting length',
            U_infty is the free stream velocity,
            nu the fluid viscosity,
            alpha the fluid thermal diffusivity,
            L length of the plate
            xi unheated started length (only applies of using unheated starting length)
            Re_xc critical Reynolds number for transition laminar to turbulence

        output: bl.Re_L Reynolds at the trailing edge of the plate (x=L)

        bl.local(x) calculates the local Re (bl.Re_x), Cf (bl.Cf_x), Nu (bl.Nu_x) and velocity
                    thermal boundary layer thicknesses (bl.delta_x and bl.delta_Tx) at x based on thermal_bc

        bl.average(x) calculates the average Cf (bl.C_fave), Nu (bl.Nu_ave) over a length x from the leading edge

        """
    def __init__(self,regime,thermal_bc,U_infty,nu,alpha,L,xi=0.0,Re_xc=5e5):
        self.regime = regime
        self.thermal_bc = thermal_bc
        self.U_infty = U_infty
        self.nu = nu
        self.alpha = alpha
        self.Pr = self.nu/self.alpha
        self.L = L
        self.xi = xi
        self.Re_xc = Re_xc
        self.Re_L = self.L*self.U_infty/self.nu
        self.x_c = self.Re_xc*self.nu/self.U_infty
        if self.regime != "laminar" and self.regime and "turbulent" and self.regime != "mixed":
            print("Warning: regime is not properly defined")
        if self.thermal_bc != "isothermal" and self.thermal_bc != "heat flux" and self.thermal_bc != "unheated starting length":
            print("Warning: thermal boundary condition is not properly defined")
        if self.Re_L > self.Re_xc and self.regime == "laminar":
            print("Warning: The end plate Reynolds number is larger than the critical Reynolds number, consider 'mixed' regime instead")
    def local(self,x):
        self.x = x
        self.Re_x = self.U_infty*self.x/self.nu
        if x == 0.:
            self.delta_x = 0.
            self.delta_Tx = 0.
            self.Cf_x = 0.
            self.Nu_x = 0.
        else:
            if self.regime == "laminar":
                self.delta_x = 5.0*self.x/np.sqrt(self.Re_x)
                self.Cf_x = 0.664*np.power(self.Re_x,-1./2.)
                if self.thermal_bc == "isothermal":
                    self.Nu_x = 0.332*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)
                elif self.thermal_bc == "heat flux":
                    self.Nu_x = 0.453*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)
                elif self.thermal_bc == "unheated starting length":
                    self.Re_xi = self.xi*self.U_infty/self.nu
                    self.Nu_x = 0.332*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)/\
                            np.power(1.-np.power(self.xi/self.x,3./4.),1./3.)
            elif self.regime == "turbulent":
                self.delta_x = 0.37*self.x*np.power(self.Re_x,-1./5.)
                self.Cf_x = 0.0592*np.power(self.Re_x,-1./5.)
                if self.thermal_bc == "isothermal":
                    self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)
                elif self.thermal_bc == "heat flux":
                    self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)
                elif self.thermal_bc == "unheated starting length":
                    self.Re_xi = self.xi*self.U_infty/self.nu
                    self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)/\
                            np.power(1.-np.power(self.xi/self.x,9./10.),1./9.)
            elif self.regime == "mixed":
                if self.x < self.x_c:
                    self.delta_x = 5.0*self.x/np.sqrt(self.Re_x)
                    self.Cf_x = 0.664*np.power(self.Re_x,-1./2.)
                    if self.thermal_bc == "isothermal":
                        self.Nu_x = 0.332*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)
                    elif self.thermal_bc == "heat flux":
                        self.Nu_x = 0.453*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)
                    elif self.thermal_bc == "unheated starting length":
                        self.Re_xi = self.xi*self.U_infty/self.nu
                        self.Nu_x = 0.332*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)/\
                            np.power(1.-np.power(self.xi/self.x,3./4.),1./3.)
                else:
                    self.delta_x = 0.37*self.x*np.power(self.Re_x,-1./5.)
                    self.Cf_x = 0.0592*np.power(self.Re_x,-1./5.)
                    if self.thermal_bc == "isothermal":
                        self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)
                    elif self.thermal_bc == "heat flux":
                        self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)
                    elif self.thermal_bc == "unheated starting length":
                        self.Re_xi = self.xi*self.U_infty/self.nu
                        self.Nu_x = 0.0296*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)/\
                            np.power(1.-np.power(self.xi/self.x,9./10.),1./9.)

            self.delta_Tx = self.delta_x*np.power(self.Pr,-1./3.)
    def average(self,x):
        self.x = x
        self.Re_x = self.U_infty*self.x/self.nu
        if x == 0.:
            print("The length cannot be zero")
        if self.regime == "laminar":
            self.Cf_ave = 1.328*np.power(self.Re_x,-1./2.)
            if self.thermal_bc == "isothermal" or self.thermal_bc == "heat flux":
                self.Nu_ave = 0.664*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)
            elif self.thermal_bc == "unheated starting length":
                p = 2.
                self.Re_xi = self.xi*self.U_infty/self.nu
                self.Nu_ave = 0.664*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)*\
                              x/(x-self.xi)*np.power(1.-np.power(self.xi/x,(p+1.)/(p+2.)),p/(p+1.))
        elif self.regime == "turbulent":
            self.Cf_ave = 0.074*np.power(self.Re_x,-1./5.)
            if self.thermal_bc == "isothermal" or self.thermal_bc == "heat flux":
                self.Nu_ave = 0.037*np.power(self.Re_x,4./5.)*np.power(self.Pr,1./3.)
            elif self.thermal_bc == "unheated starting length":
                p = 8.
                self.Re_xi = self.xi*self.U_infty/self.nu
                self.Nu_ave = 0.664*np.power(self.Re_x,1./2.)*np.power(self.Pr,1./3.)*\
                              x/(x-self.xi)*np.power(1.-np.power(self.xi/x,(p+1.)/(p+2.)),p/(p+1.))
        elif self.regime == "mixed":
            A = 0.037*np.power(self.Re_xc,4./5.)-0.664*np.power(self.Re_xc,1./2.)

            self.C_fave = 0.074*np.power(self.Re_x,-1./5.) - 2.*A/self.Re_x
            self.Nu_ave = (0.037*np.power(self.Re_x,4./5.) - A)*np.power(self.Pr,1./3.)

class CircularCylinder(object):
    """ Nusselt correlations for cylinders
    import HT_external_convection.py as extconv

    bluff_body =extconv.CircularCylinder(correlation,Re,Pr,Pr_s = 0.0)
        where Re, Pr, and Pr_s are the Reynolds number, Prandtl number of the flow and surface Prandtl numbers, respectively. If using Hilpert of Churchill Bernstein correlations,
        Re and Pr must be defined at film temperature, Pr_s can be set to anything since it is not used.
        If using Zukauskas, Re and Pr are defined at temperature at infinity.
        correlation may be 'Hilpert', 'Churchill-Bernstein', 'Zukauskas'
        Example:
        bluff_body = extconv.CircularCylinder('Hilpert',Re,Pr)
        bluff_body = extconv.CircularCylinder('Churchill-Bernstein',Re,Pr)
        bluff_body = extconv.CircularCylinder('Zukauskas',Re,Pr,Pr_s = xx)

        Output: bluff_body.Nu average Nusselt number also bluff_body.correlation, bluff_body.Re, bluff_body.Pr, bluff_body.Pr_s

    bluff_body.correlation('Name of the correlation')
    Name of the correlation may be 'Hilpert', 'Churchill-Bernstein', 'Zukauskas'

    """
    def __init__(self,correlation,Re,Pr,Pr_s = 0.0):
        self.correlation = correlation
        self.Re = Re
        self.Pr = Pr
        self.Pr_s = Pr_s
        if correlation == "Zukauskas" and Pr_s == 0.0:
            print("Warning: Zukauskas correlation requires Pr_s")
        if self.correlation == "Hilpert":
            if self.Re < 0.4:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu = 0.
            elif  self.Re < 4.:
                C = 0.989
                m = 0.33
            elif  self.Re < 40:
                C = 0.911
                m = 0.385
            elif self.Re < 4000:
                C = 0.683
                m = 0.466
            elif self.Re < 40000.:
                C = 0.193
                m = 0.618
            elif self.Re <= 400000.:
                C = 0.027
                m = 0.805
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation")
                self.Nu = 0.
            if self.Re >= 0.4 and self.Re <= 400000.:
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
        elif self.correlation == "Churchill-Bernstein":
            if (self.Re*self.Pr < 0.2):
                print("Warning: Product RePr lower than acceptable limit for Churchill Bernstein Correlation")
                self.Nu = 0.

            else:
                self.Nu = 0.3+(0.62*self.Re**(0.5)*self.Pr**(1./3.)) \
                  /(1.+(0.4/self.Pr)**(2./3.))**(1./4.) \
                *(1.+(self.Re/282000.)**(5./8.))**(4./5.)
        elif self.correlation == "Zukauskas":
            if (self.Pr <= 10):
                n = 0.37
            else:
                n = 0.36
            if (self.Re < 1.) and (self.Re > 1.e6):
                print("Warning Reynolds number out of bounds for the Zukauskas Correlation")
                self.Nu = 0.
            else:
                if (self.Re < 40.):
                    C = 0.75
                    m = 0.4
                elif (self.Re < 1000.):
                    C = 0.51
                    m = 0.5
                elif (self.Re < 2.e5):
                    C = 0.26
                    m = 0.6
                else:
                    C = 0.076
                    m = 0.7
                self.Nu = C*self.Re**m*self.Pr**n*(self.Pr/self.Pr_s)**(1./4.)

class NonCircularCylinder(object):
    """ Nusselt correlations for  cylinders with non circular cross-sections.
    import HT_external_convection.py as extconv

    bluff_body =extconv.NonCircularCylinder(geometry,Re,Pr) where
    geometry = "angled square" square with stagnation point on one of its edges
               "square" square with stagnation point at the center of one of its faces
               "angled hexagon" hexagon with stagnation point on one of its edges
               "hexagon" hexagon with stagnation point at the center of one of its faces
               "thin plate" thin plate perpendicular to the flow
    Re: Reynolds number at film temperature
    Pr: Prandtl number at film temperature

    Output: bluff_body.Nu, bluff_body.Nu_front, bluff_body.Nu_back, the last two are for thin plate only
    also bluff_body.geometry, bluff_body.Re, bluff_body.Pr

    """
    def __init__(self,geometry,Re,Pr):
        self.geometry = geometry
        self.Re = Re
        self.Pr = Pr
        if self.geometry == "angled square":
            self.Nu_front = np.inf
            self.Nu_back = np.inf
            if self.Re < 6000:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu = np.inf
            elif  self.Re <= 60000.:
                C = 0.304
                m = 0.59
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation")
                self.Nu = np.inf
        elif self.geometry == "square":
            self.Nu_front = np.inf
            self.Nu_back = np.inf
            if self.Re < 5000:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu = np.inf
            elif  self.Re <= 60000.:
                C = 0.158
                m = 0.66
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation")
                self.Nu = np.inf
        elif self.geometry == "angled hexagon":
            self.Nu_front = np.inf
            self.Nu_back = np.inf
            if self.Re < 4500:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu = np.inf
            elif  self.Re <= 90700.:
                C = 0.150
                m = 0.638
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation")
                self.Nu = np.inf
        elif self.geometry == "hexagon":
            self.Nu_front = np.inf
            self.Nu_back = np.inf
            if self.Re < 5200:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu = np.inf
            elif  self.Re <= 20400.:
                C = 0.164
                m = 0.638
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
            elif  self.Re <= 105000.:
                C = 0.039
                m = 0.78
                self.Nu = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation")
                self.Nu = np.inf
        elif self.geometry == "thin plate":
            self.Nu = np.inf
            if self.Re < 10000:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu_front = np.inf
            elif  self.Re <= 50000.:
                C = 0.667
                m = 0.5
                self.Nu_front = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation for Nu_front")
                self.Nu_back = np.inf
            if self.Re < 7000:
                print("Warning, Reynolds number too low for Hilpert Correlation")
                self.Nu_back = np.inf
            elif  self.Re <= 80000.:
                C = 0.191
                m = 0.667
                self.Nu_back = C * self.Re**m * self.Pr**(1./3.)
            else :
                print("Warning Reynolds number is too high for the Hilpert Correlation for Nu_front")
                self.Nu_back = np.inf

class BankofTubes(object):
    """ Nusselt correlations for flow across banks of tubes
    import HT_external_convection.py as extconv
    bank = extconv.BankofTubes('aligned','air',T_i,T_s,T_o,"C",V_i,D,S_L,S_T,N_L=1)
    **Input:**
    arrangement = "aligned" tubes are aligned in row and column
                  "staggered" tubes are staggered from one row to the next
    fluidspecie = 'air', 'water' or any other specie available in library thermodynamics
    T_i = inlet temperature
    T_s = tube surface temperature
    T_o = outlet temperature, provide a guess < T_s if unknown
    V_i: Inlet velocity
    S_L: tube center to tube center separation  between two consecutive rows (perpendicular to the flow)
    S_T: tube center to tube center separation  between two consecutive rows (aligned with the flow)
    N_L: number of rows aligned with flow if unknown give your best guess (default =1)

    Output:
    bank.T_m: arithmetic mean of T_i and T_o
    bank.Nu: average Nusselt number
    bank.Delta_T_lm: Log mean temperature difference
    bank.rho_i: inlet fluid density
    bank.mu_m: fluid viscosity at T_m
    bank.k_m: fluid thermal conductivity at T_m
    bank.Cp_m: Specific heat at T_m
    bank.Pr_m: Prandtl number at T_m
    bank.Pr_s: Prandtl number at T_s
    bank.Vmax: Max fluid velocity based on arrangement
    bank.Re: Reynolds number of the system

    bank.Nu: average Nusselt number
    bank.hbar: average heat transfer coefficient

    Functions:
    bank.heat_rate(N_T,N_L,L=1) creates bank.q, the heat rate per tube length is L is omitted
                or total heat rate if L is provided, based on the average convection coefficient
    bank.temperature_outlet_tube_banks(N_T,N_L) overwite bank.T_o (useful if T_o is unkown and you provided a
        guess. Rerun the object with new T_o to adjust Nu)
    bank.pressure_drop(N_L,f,chi) creates bank.Delta_p the pressure drop across the bank. f and chi must
        be extrapolated from graphs (see book or notebook)

    """
    def __init__(self,arrangement,fluidspecie,T_i,T_s,T_o,unit,V_i,D,S_L,S_T,N_L=1):
        self.arrangement = arrangement
        self.T_i = T_i
        self.T_s = T_s
        self.V_i = V_i
        if (T_o == T_s):
            print("T_o and T_s cannot be equal, setting T_o to (T_i+T_s)/2 (think of it as your first guess)")
            T_o = (T_i+T_s)/2.
        self.T_o = T_o
        T_m = (T_i + T_o)/2.
        self.T_m = T_m
        self.Delta_T_lm = ((T_s-T_i)-(T_s-T_o))/np.log((T_s-T_i)/(T_s-T_o))
        self.fluid = fluidspecie
        fluid_i = thermo.Fluid(fluidspecie,T_i,unit)
        fluid_m = thermo.Fluid(fluidspecie,T_m,unit)
        fluid_s = thermo.Fluid(fluidspecie,T_s,unit)
        self.rho_i = fluid_i.rho
        self.mu_m = fluid_m.mu
        self.k_m = fluid_m.k
        self.Cp_m = fluid_m.Cp
        self.Pr_m = fluid_m.nu/fluid_m.alpha
        self.Pr_s = fluid_s.nu/fluid_s.alpha
        self.S_L = S_L
        self.S_T = S_T
        self.N_L = N_L
        self.D = D
        if self.arrangement == 'aligned':
            self.Vmax = self.S_T*self.V_i/(self.S_T-D)
        elif self.arrangement == 'staggered':
            self.S_D = np.sqrt(self.S_L**2+(self.S_T/2.)**2)
            self.Vmax = self.S_T*V_i/(2.*(self.S_D-D))
        Re = self.rho_i*self.Vmax*self.D/self.mu_m
        self.Re = Re
        self.Nu = np.inf
        Corr_aligned = np.array([0.70,0.80,0.86,0.90,0.92,0.94,0.95,0.96,0.96,0.97,0.97,0.97,0.98,0.99,0.99,0.99,0.99,0.99,0.99])
        Corr_staggered = np.array([0.64,0.76,0.84,0.89,0.92,0.94,0.95,0.96,0.96,0.97,0.97,0.97,0.98,0.99,0.99,0.99,0.99,0.99,0.99])
        if (N_L < 20):
            if arrangement == 'aligned':
                Corr = Corr_aligned[N_L-1]
            elif arrangement == 'staggered':
                Corr = Corr_staggered[N_L-1]
        else:
            Corr = 1.
        if (Re < 10.):
            print('Warning: Re is out of bounds')
        if (Re >= 10.) and (Re <= 100.):
            if arrangement == 'aligned':
                C = 0.8
                m = 0.4
            elif arrangement == 'staggered':
                C = 0.9
                m = 0.4
            self.Nu = Corr*C*self.Re**m*self.Pr_m**(0.36)*(self.Pr_m/self.Pr_s)**(1./4.)
        elif (Re > 100.) and (Re <= 1000.):
            C = 0.51
            m = 0.
            self.Nu = Corr*C*self.Re**m*self.Pr_m**(0.36)*(self.Pr_m/self.Pr_s)**(1./4.)
        elif (Re > 1000.) and (Re <= 2.e5):
            if arrangement == 'aligned':
                if (S_T/S_L > 0.7):
                    C = 0.27
                    m = 0.63
                else:
                    print('Warning: inefficient, S_T/S_L<0.7')

            elif arrangement == 'staggered':
                if (S_T/S_L < 2):
                    C = 0.35*(S_T/S_L)**(1./5.)
                    m = 0.6
                else:
                    C = 0.40
                    m = 0.6
            self.Nu = Corr*C*self.Re**m*self.Pr_m**(0.36)*(self.Pr_m/self.Pr_s)**(1./4.)
        elif (Re > 2e5) and (Re <= 2.e6):
            if arrangement == 'aligned':
                C = 0.021
                m = 0.84
            elif arrangement == 'staggered':
                C = 0.022
                m = 0.84
            self.Nu = Corr*C*self.Re**m*self.Pr_m**(0.36)*(self.Pr_m/self.Pr_s)**(1./4.)
        else:
            print('Warning: Re is out of bounds')

        self.hbar = self.Nu*self.k_m/self.D
        self.N_L_for_given_To = -np.log((self.T_s-self.T_o)/(self.T_s-self.T_i))/ \
            (np.pi*self.D*self.hbar)*(self.rho_i*self.V_i*self.S_T*self.Cp_m)
        if (self.N_L < 20) and (self.N_L_for_given_To >= 20):
            print("WARNING input N_L < 20 but N_L computed for input T_o >=20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")
        if (self.N_L < 20) and (self.N_L_for_given_To >= 20):
            print("WARNING input N_L >= 20 but N_L computed for input T_o <20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")

    def heat_rate(self,N_T,N_L,L=1):
        N = N_T*N_L
        if N_L > 20 and self.N_L < 20:
            print("WARNING: you chose N_L > 20 but your initial guess was < 20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")
        elif N_L < 20 and self.N_L >= 20:
            print("WARNING: you chose N_L < 20 but your initial guess was > 20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")
        self.q=N*self.hbar*np.pi*self.D*self.Delta_T_lm*L

    def temperature_outlet_tube_banks(self,N_T,N_L):
        if N_L >= 20 and self.N_L < 20:
            print("WARNING: you chose N_L > 20 but your initial guess was < 20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")
        elif N_L < 20 and self.N_L >= 20:
            print("WARNING: you chose N_L < 20 but your initial guess was > 20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu")
        N = N_T*N_L
        self.T_o = self.T_s-(self.T_s-self.T_i)* \
            np.exp(-np.pi*self.D*N*self.hbar/(self.rho_i*self.V_i*N_T*self.S_T*self.Cp_m))
    def pressure_drop(self,N_L,f,chi):
        self.Delta_p = N_L*chi*(self.rho_i*self.Vmax**2/2)*f

```

    Overwriting HT_external_convection.py

```python

import HT_external_convection as extconv

square = extconv.NonCircularCylinder("thin plate",30000.,0.7)
print(square.Nu_back)
```

    164.30064088485295

```python

D = 10.e-3
S_T = 15.e-3
S_L = S_T
N_L = 14
N_T = 14
N = 196
T_i = 25.
V_i = 5.
T_s = 100.
T_o = T_s - 10.
L = 1.
import thermodynamics as thermo
bank = extconv.BankofTubes('aligned','air',T_i,T_s,T_o,"C",V_i,D,S_L,S_T,N_L)

print(bank.Re)
print(bank.Nu)
print(bank.hbar)
bank.heat_rate(N_T,N_L,L)
print(bank.q)
bank.temperature_outlet_tube_banks(N_T,N_L)
print(bank.T_o)
T_o = bank.T_o
print("New iteration")
bank = extconv.BankofTubes('aligned','air',T_i,T_s,T_o,"C",V_i,D,S_L,S_T,N_L)

print(bank.Re)
print(bank.Nu)
print(bank.hbar)
bank.heat_rate(N_T,N_L,L)
print(bank.q)
bank.temperature_outlet_tube_banks(N_T,N_L)
print(bank.T_o)
bank.pressure_drop(N_L,0.32,1.)
print(bank.Delta_p)
# h = bank.Nu*air.k/D
# T_o = extconv.temperature_outlet_tube_banks(T_s,T_i,D,N,N_T,h,air.rho,V_i,S_T,air.Cp)
# DT_lm = extconv.Delta_T_lm(T_s,T_i,T_o)
# bank.heat_rate(h,D,T_s,T_i,T_o)
# print(bank.q)
# print(T_o)
```

    WARNING input N_L < 20 but N_L computed for input T_o >=20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu
    WARNING input N_L >= 20 but N_L computed for input T_o <20. \ Rerun your BankofTubes object with the appropriate N_L to calculate the correct Nu
    8875.203078134544
    73.02371543088573
    206.83967395798382
    41086.48584005276
    72.80755683113756
    New iteration
    9077.946763871732
    73.94647560894492
    205.00377013428093
    59482.787646596495
    72.60865494226965
    597.4920000000001

```python

D = 10.e-3
S_T = 15.e-3
S_L = S_T
N_L = 14
N_T = 14
N = 196
T_i = 25.
V_i = 5.
T_s = 100.
T_o = 75.
L = 1.
import thermodynamics as thermo
bank = extconv.BankofTubes('aligned','air',T_i,T_s,T_o,"C",V_i,D,S_L,S_T,N_L)

print(bank.Re)
print(bank.Nu)
print(bank.hbar)
print(bank.N_L_for_given_To)
N_L = int(bank.N_L_for_given_To) + 1
bank.heat_rate(N_T,N_L,L)
print(bank.q)
bank.temperature_outlet_tube_banks(N_T,N_L)
print(bank.T_o)
# T_o = bank.T_o
# print("New iteration")
# bank = extconv.BankofTubes('aligned','air',T_i,T_m,T_s,T_o,"C",V_i,D,S_L,S_T,N_L)

# print(bank.Re)
# print(bank.Nu)
# print(bank.hbar)
# bank.heat_rate(N_T,N_L,L)
# print(bank.q)
# bank.temperature_outlet_tube_banks(N_T,N_L)
# print(bank.T_o)
# bank.pressure_drop(N_L,0.32,1.)
# print(bank.Delta_p)
# # h = bank.Nu*air.k/D
# # T_o = extconv.temperature_outlet_tube_banks(T_s,T_i,D,N,N_T,h,air.rho,V_i,S_T,air.Cp)
# # DT_lm = extconv.Delta_T_lm(T_s,T_i,T_o)
# # bank.heat_rate(h,D,T_s,T_i,T_o)
# # print(bank.q)
# # print(T_o)
```

    9051.578344318328
    73.82744472007458
    205.2402963218073
    15.25542247062167
    65733.39689091778
    76.30520476189815

```python

D = 10.e-3
S_T = 15.e-3
S_L = S_T
N_L = int(np.int(np.sqrt(196)))
N_T = int(np.int(np.sqrt(196)))
N = 196
T_i = 25.
V_i = 5.
T_s = 100.
T_o = 75
T_m = (T_i+T_o)/2.
import thermodynamics as thermo
air = thermo.Fluid("air",thermo.C2K(T_m))
air_s = thermo.Fluid("air",thermo.C2K(T_s))

import HT_external_convection as extconv

?extconv.BankofTubes

nu = air.nu
Pr = air.Pr
Pr_s = air_s.Pr

bank = extconv.BankofTubes("aligned",V_i,D,nu,Pr,Pr_s,S_L,S_T,N_L,N_T)
print(bank.Re)
print(bank.Nu)
h = bank.Nu*air.k/D
T_o = extconv.temperature_outlet_tube_banks(T_s,T_i,D,N,N_T,h,air.rho,V_i,S_T,air.Cp)
DT_lm = extconv.Delta_T_lm(T_s,T_i,T_o)
bank.heat_rate(h,D,T_s,T_i,T_o)
print(bank.q)
print(T_o)
```

    7830.853563038373
    67.4638616535179
    55034.79484540479
    74.06985116378564

### Library of Nu correlations and functions for internal flow in pipes

```python
%%file HT_internal_convection.py
"""
Object name: PipeFlow
"""
import numpy as np
import scipy
import scipy.optimize

class PipeFlow(object):
    """ Determination of Nu, pressure drop, mean temperature for internal convection
        import HT_internal_convection.py as intconv

        pipe =intconv.PipeFlow(D, Re=0.0, Um = 0.0, mdot = 0.0, nu = 0.0, rho = 0.0)
        where
        D is the only required input and one of the following combination (Re, nu) or (Um, nu) or (mdot, rho, nu)
        Hence the minimum calls for PipeFlow are
        pipe =intconv.PipeFlow(D, Re= Re_m, nu = nu_m) outputs pipe.Um
        pipe =intconv.PipeFlow(D, Re= Re_m, nu = nu_m, rho = rho_m) outputs pipe.Um (bulk velocity)
                and pipe.mdot (mass flow)
        pipe =intconv.PipeFlow(D, Um = 0.0, nu = 0.0) outputs pipe.Re
        pipe =intconv.PipeFlow(D, Um = Um, nu = nu_m, rho = rho_m) outputs pipe.Re, pipe.mdot
        pipe =intconv.PipeFlow(D, mdot = 0.0, nu = 0.0, rho = 0.0) outputs pipe.Re, pipe.Um

        pipe.f_laminar(Re) outputs the friction factor for laminar flow pipe.f
        pipe.f_turbulent(Re,eps = 0.0, nu = 0.0) outputs the friction factor for turbulent flow pipe.f

        The following correlations output pipe.Nu
        pipe.laminar_isothermal for isothermal wall boundary condition
        pipe.laminar_isoflux for isoflux wall boundary condition
        pipe.Dittus_Boelter(mode, Pr, Re = 0.) for turbulent flow where mode is either "heating" or "cooling"
            The Re is optional if omitted, the Reynolds number calculated in the object PipeFlow will be used
        pipe.Sieder_Tate(Pr,mu,mu_s, Re = 0.0) mu and mu_s are the mean and wall dynamics viscosities
            The Re is optional if omitted, the Reynolds number calculated in the object PipeFlow will be used
        pipe.Gnielinski( Pr, f,Re = 0.0): where f is the friction factor
            The Re is optional if omitted, the Reynolds number calculated in the object PipeFlow will be used

        """
    def __init__(self,D, Re=0.0, Um = 0.0 , mdot = 0.0, nu = 0.0, rho = 0.0, L = 1.0 ):
        self.D = D
        self.L = L

        if Re == 0.0:
            if Um != 0.0 and nu != 0.0:
                Re = Um*D/nu
            elif mdot != 0 and rho != 0.0 and nu != 0.0:
                Um = mdot/(rho*np.pi*D**2/4.)
                Re = Um*D/nu
            else:
                print("Warning if Re == 0, Um, D and nu or mdot, rho and nu must be specified")

        self.Re = Re
        if Um == 0.:
            if Re != 0. and nu != 0.:
                Um = Re*nu/D
                if mdot == 0.0 and rho != 0.0:
                    mdot = rho*Um*np.pi*D**2/4.
            elif mdot !=0.0 and rho != 0.0:
                Um = mdot/(rho*np.pi*D**2/4.)

        self.Um = Um
        if mdot == 0.0:
            if rho != 0.0:
                mdot = rho*Um*np.pi*D**2/4.
            else:
                self.rho = 1.0
                self.mdot = rho*Um*np.pi*D**2/4.
        self.mdot = mdot
        self.nu = nu
        if Re == 0. and nu != 0.:
            Re = Um*D/nu
        self.Re = Re

        if rho == 0.0:
            self.rho = 1.0

        else:
            self.rho = rho

    def f_laminar(self, Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        elif Re == 0 and self.Re == 0.0:
            print("Warning Reynolds number is not defined")
        self.f = 64./Re
        self.dPdx = self.f*(self.L/self.D)*(self.rho*self.Um**2)/2.

    def f_turbulent(self,Re = 0.0, eps = 0.0):
        if Re == 0. and self.Re !=0.0:
            Re = self.Re
        elif Re == 0 and self.Re == 0.0:
            print("Warning Reynolds number is not defined")
        if eps == 0.0:
            print("Pipe wall is assumed to be hydrodynamically smooth")
        e = eps

        f_0 = (0.790*np.log(Re)- 1.64)**(-2.)
        if (e > 0.):
            f_1 = 1./(-2.0*np.log10(e/3.71))**2
        else:
            f_1 = f_0
        f_guess = np.max([f_0,f_1])
        #f_guess = 0.04
        def f_tmp(x):
            y = (-2*np.log10((2.51/(Re*np.sqrt(x))) + (e/(3.71))) - 1.0/np.sqrt(x))
            return y
        y = scipy.optimize.fsolve(f_tmp, f_guess)
        self.f = y[0]
        self.dPdx = self.f*(self.L/self.D)*(self.rho*self.Um**2)/2.

    def laminar_isothermal(self):
        self.Nu = 3.66

    def laminar_isoflux(self):
        self.Nu = 4.36

    def Dittus_Boelter(self,mode,Pr,Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        else:
            print("Warning Reynolds number is not defined")
        if (mode == 'heating'):
            n = 0.4
        elif (mode == 'cooling'):
            n = 0.3
        else:
            print("Warning you have to specify mode='heating' or 'cooling'")
        self.Nu = 0.023*Re**(4./5.)*Pr**n

    def Sieder_Tate(self,Pr,mu,mu_s, Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        else:
            print("Warning Reynolds number is not defined")
        self.Nu = 0.027*Re**(4/5)*Pr**(1/3)*(mu/mu_s)**0.14

    def Gnielinski(self, Pr, f,Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        else:
            print("Warning Reynolds number is not defined")
        self.Nu = (f/8.)*(Re-1000.)*Pr/(1+12.7*(f/8.)**0.5*(Pr**(2./3.)-1.))

    def Skupinski(self,Pr, Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        else:
            print("Warning Reynolds number is not defined")
        self.Nu = 4.82+0.0185*(Re*Pr)**0.827

    def Seban(self,Pr, Re = 0.0):
        if Re == 0. and self.Re !=0:
            Re = self.Re
        else:
            print("Warning Reynolds number is not defined")
        self.Nu = 5.0+0.025*(Re*Pr)**0.8

def log_mean_temperature(T_s,T_o,T_i):
    if (T_s < min(T_o,T_i)):
        DT_o = T_o-T_s
        DT_i = T_i-T_s
    elif (T_s > max(T_o,T_i)):
        DT_o = T_s-T_o
        DT_i = T_s-T_i
    return (DT_o-DT_i)/np.log(DT_o/DT_i)

def T_mx_Ts_constant(T_s,T_mi,P,mdot,Cp,hbar,x):
    return T_s-(T_s-T_mi)*np.exp(-P*x*hbar/(mdot*Cp))

def T_mo_T_infty(T_infty,T_mi,P,L,mdot,Cp,R_tot):
    return T_infty-(Tinfty-T_mi)*np.exp(-1/(mdot*Cp*Rtot))

```

    Overwriting HT_internal_convection.py

```python
import HT_internal_convection as intconv

pipe = intconv.PipeFlow(D=0.01, L= 1., rho=1000., nu=1e-6, Um=2)

pipe.f_turbulent()
print(pipe.f,pipe.dPdx, pipe.Re)
```

    Pipe wall is assumed to be hydrodynamically smooth
    0.025883078538096054 5176.615707619211 20000.0

### Library for natural convection around cylinders

```python
%%file HT_natural_convection.py
"""
Object name: HorizontalCylinder
Functions: Gr(g,beta,DT,D,nu) gives the Grashoff number based on:
            gravity g, thermal expansion coefficient beta, Temperature difference DT,
            length scale D, viscosity nu
           Ra(g,beta,DT,D,nu,alpha) gives the Rayleigh number where alpha is the thermal conductivity.
"""
import numpy as np
import scipy
import scipy.optimize

class HorizontalCylinder(object):
    """ Natural convection about a horizontal cylinder
        from NewLibraries import HT_natural_convection as natconv
        cyl = natconv.HorizontalCylinder(correlation, Ra, Pr = 0.0)
        where correlation is "Morgan" or "Churchill-Chu"
        cyl = natconv.HorizontalCylinder("Morgan", Ra)
        cyl = natconv.HorizontalCylinder("Churchill-Chu", Ra, Pr = xx)
    """

    def __init__(self,correlation="Morgan", Ra=0.0, Pr = 0.0):
        self.correlation = correlation
        self.Ra = Ra
        if correlation == "Morgan":
            if (Ra <= 1e-2):
                C=0.675
                n=0.058
            elif (Ra <= 1e2):
                C=1.02
                n=0.148
            elif (Ra <= 1e4):
                C=0.85
                n=0.188
            elif (Ra <= 1e7):
                C=0.480
                n=0.250
            elif (Ra <= 1e12):
                C=0.125
                n=0.333
            self.Nu = C*Ra**n
        elif correlation == "Churchill-Chu":
            if Pr == 0.:
                print("Warning you must specify Pr for Churchill and Chu correlation")
            else:
                self.Nu = (0.60+(0.387*Ra**(1./6.))/(1.+(0.559/Pr)**(9./16.))**(8./27.))**2
        else:
            print("Warning wrong correlation name")

class VerticalEnclosure(object):
    """ Natural convection about a horizontal cylinder
        from NewLibraries import HT_natural_convection as natconv
        cyl = natconv.HorizontalCylinder(correlation, Ra, Pr = 0.0)
        where correlation is "Morgan" or "Churchill-Chu"
        cyl = natconv.HorizontalCylinder("Morgan", Ra)
        cyl = natconv.HorizontalCylinder("Churchill-Chu", Ra, Pr = xx)
    """

    def __init__(self,Ra,Pr,H,L):
        self.Ra = Ra
        self.Pr = Pr
        self.H = H
        self.L = L
        if correlation == "Morgan":
            if (H/L) < 2.:
                if Ra*Pr/(0.2+Pr)> 1.e3:
                    self.Nu = 0.18*(Pr/(0.2+Pr)*Ra)**0.29
                else:
                    print('Ra is too low for this correlation')
                    self.Nu = np.inf
            elif H/L < 10:
                if Ra < 1e10:
                    self.Nu = 0.22*(Pr/(0.2+Pr)*Ra)**0.28*(H/L)**(-0.25)
                else:
                    print('Ra is too high for this correlation')
                    self.Nu = np.inf
            elif Ra < 1e4:
                print('Ra is too low for this correlation')
                self.Nu = np.inf
            elif Ra < 1e7:
                if Pr > 0.6 and Pr < 2e4:
                    print('ok')
                    self.Nu =0.42*Ra**0.25*Pr**0.012*(H/L)**(-0.3)
                else :
                    print('Pr is out of bounds for this correlation')
                    self.Nu = np.inf
            elif Ra < 1e9:
                if Pr > 0.6 and Pr < 20.:
                    self.Nu =0.46*Ra**(1./3.)
                else :
                    print('Pr is out of bounds for this correlation')
                    self.Nu = np.inf
            else:
                print('Ra is too high, got nothing for you')
                self.Nu = np.inf

def Gr(g=9.81,beta=0.0,DT=0.0,D=0.0,nu=1.0):
    return (g*beta*DT*D**3)/(nu**2)

def Ra(g=9.81,beta=0.0,DT=0.0,D=0.0,nu=1.0,alpha=1.0):
    return (g*beta*DT*D**3)/(nu*alpha)

```

    Overwriting HT_natural_convection.py

```python

```


# 变分量子本征求解器

<em> Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved. </em>

## 概览

目前普遍认为，量子计算在近期很有前景的一个应用是处理量子化学问题 [1-2]。**变分量子本征求解器** （VQE）作为这个研究方向的核心应用之一，为研究者们提供了可以在目前含噪的中等规模量子设备（NISQ device）上研究量子化学的可能 [1-4]。其核心任务是求解一个量子尺度上封闭物理系统的哈密顿量 $\hat{H}$ 的基态能量及其对应的量子态。主要的实现方法是通过在量子设备上准备一个参数化的试探波函数 $|\Psi(\boldsymbol\theta)\rangle$ 然后结合经典机器学习中的优化算法（例如梯度下降法）去不断地调整、优化参数 $\boldsymbol\theta$ 使得期望值  $\langle \Psi(\boldsymbol\theta)|\hat{H}|\Psi(\boldsymbol\theta)\rangle$ 最小化。这套方案的基本原理是基于 **Rayleigh-Ritz 变分原理**。

$$
E_0 = \min_{\boldsymbol\theta} \langle \Psi(\boldsymbol\theta)|\hat{H}|\Psi(\boldsymbol\theta)\rangle.
\tag{1}
$$

其中 $E_0$ 表示该系统的基态能量。从数值分析的角度来看，该问题可以被理解为求解一个**离散化**哈密顿量 $H$（埃尔米特矩阵）的最小本征值 $\lambda_{\min}$ 和其对应的本征向量 $|\Psi_0\rangle$。具体的离散化过程是如何通过建立模型实现的，这属于量子化学的专业领域范畴。精确地解释该过程需要很长的篇幅，这超过了本教程所能处理的范围。我们会在下一节背景知识模块粗略的介绍一下相关知识，感兴趣的读者可以参考 `量子化学: 基本原理和从头计算法`系列丛书 [5]。通常来说，为了能在量子设备上处理量子化学问题，哈密顿量 $H$ 会被表示成为泡利算符 $\{X,Y,Z\}$ 的加权求和形式。

$$
H = \sum_k c_k ~ \bigg( \bigotimes_{j=0}^{M-1} \sigma_j^{(k)} \bigg),
\tag{2}
$$

其中 $c_k$ 表示权重系数, $\sigma_j^{(k)} \in \{I,X,Y,Z\}$ 并且 $M$ 表示所需的量子比特个数。这样一种哈密顿量的表示形式被称为 **泡利字符串**。以下为一个2量子比特的具体例子，

$$
H= 0.12~Y_0 \otimes I_1-0.04~X_0\otimes Z_1.
\tag{3}
$$

在下一节，我们会补充一些关于电子结构问题的背景知识。本质上讨论的就是上述哈密顿量 $H$ 是如何计算的。对于熟悉相关背景的读者，或者主要关心如何在量桨上实现 VQE 的读者，请直接跳转至第三节分析氢分子（$H_2$）基态的具体例子。

## 背景： 电子结构问题

这里，我们集中讨论下量子化学中的一个基本问题 -- **电子结构问题**。更准确的说，我们关心的是给定分子（molecule）的低位能量本征态。这些信息可以帮助我们预测化学反应的速率和分子的稳定结构等等 [6]。假设一个分子由 $N_n$ 个原子核和 $N_e$ 个电子组成，描述该分子系统总能量的哈密顿量算符 $\hat{H}_{mol}$ 在一次量子化表示下可以写为，

$$
\begin{align}
\hat{H}_{\text{mol}} & = -\sum_{i}\frac{\nabla_{R_i}^2}{2M_i} - \sum_{i} \frac{\nabla_{r_i}^2}{2} -\sum_{i,j}\frac{Z_i}{\lvert R_i - r_j\lvert} + \sum_{i,j>i}\frac{Z_iZ_j}{\lvert R_i - R_j\lvert} + \sum_{i, j>i}\frac{1}{\lvert r_i - r_j\lvert},
\tag{4}
\end{align}
$$

其中 $R_i、M_i$ 和 $Z_i$ 分别表示第 $i$ 个原子核的位置、质量和原子序数（原子核内质子数），第 $i$ 个电子的位置则表示为 $r_i$。以上公式右边前两项分别代表原子核和电子的总动能。第三项表示带正电的质子和带负电的电子之间的库伦相互吸引作用。最后两项则表示原子核-原子核之间，电子-电子之间的相互排斥作用。这里，分子哈密顿量 $\hat{H}_\text{mol}$ 使用的是原子单位制能量 **哈特里能量**（Hartree），记为 Ha。$1$ 哈特里能量的大小为 $[\hbar^2/(m_ee^2a_0^2)] = 27.2$ 电子伏或 $630$ 千卡/摩尔，其中 $m_e、e$ 和 $a_0$ 分别表示电子质量、基本电荷和玻尔半径。

**注释1：** 在处理电子结构问题时，我们不考虑自旋-轨道耦合以及超精细结构。如果出于计算需要，可以作为微扰加入。

### 玻恩-奥本海默近似

由于原子核的质量要远大于电子，因而在同样的相互作用下电子的运动速度会比原子核快很多。所以，将原子核所处的位置看成固定 $R_i =$常数 是一种合理的近似。这种通过在时间尺度上将电子行为和原子核行为去耦合的近似处理思想被称为玻恩-奥本海默近似。作为近似的直接结果，公式（4）中原子核的动能项会被消去并且表示原子核-原子核相互排斥作用的项可以被认为是一个能量移位（这个项是与电子位置 $r_i$ 无关的）从而也可以作为常数项被忽略。经过这些步骤后，我们可以把哈密顿量近似为：

$$
\begin{align}
\hat{H}_{\text{electron}} & =  - \sum_{i} \frac{\nabla_{r_i}^2}{2} -\sum_{i,j}\frac{Z_i}{\lvert R_i - r_j\lvert} + \sum_{i, j>i}\frac{1}{\lvert r_i - r_j\lvert}
\tag{5},
\end{align}
$$

在经过以上近似后，分子中多电子结构的能级在理论上可以通过求解以下不含时薛定谔方程获得：

$$
\hat{H}_{\text{electron}} |\Psi_n \rangle = E_n |\Psi_n \rangle,
\tag{6}
$$

其中 $n$ 指代能级。值得注意的是，电子哈密顿量中电子-电子相互排斥作用的求和项数会随着电子数 $N_e$ 的增多至 $N_e(N_e-1)/2$ 项。这意味着对于一个含有16个电子的氧分子（$O_2$）我们需要计算多达120项的相互排斥作用项。 一般来说，这样的问题是无法从理论上精确求解的。正如狄拉克在 [Quantum mechanics of many-electron systems](https://royalsocietypublishing.org/doi/10.1098/rspa.1929.0094) [7] 所指出的那样，

> *The underlying physical laws necessary for the mathematical theory of a large part of physics and the whole of chemistry are thus completely known, and the difficulty is only that the exact application of these laws leads to equations much too complicated to be soluble.*
>
> -- Paul Dirac (1929)

由于解析的方法太复杂，那么我们可以采用数值方法来处理。一个最简单的数值方法（离散化方法）就是把上述作用中无限维度希尔伯特空间离散化为等间距排开的立方体晶格点。在这样一个离散化的空间里，主要运算规则为复数域的线性代数。假设空间的每个轴都离散为等间距排开的 $k$ 个点，则 $N$-电子（为了方便去掉下标 $e$）的多体波函数可以写为 [2]：

$$
|\Psi \rangle = \sum_{\mathbf{x_1}, \ldots, \mathbf{x_N}} \psi(\mathbf{x_1}, \ldots, \mathbf{x_N}) \mathcal{A}(|\mathbf{x_1}, \ldots, \mathbf{x_N}\rangle).
\tag{7}
$$

其中坐标 $|\mathbf{x_j}\rangle = |r_j\rangle |\sigma_j\rangle$ 记录第 $j$ 个电子的空间位置信息和自旋，$|r_j\rangle  = |x_j,y_j,z_j\rangle$ 且 $j\in \{1,2,\cdots,N\}$, $x_j,y_j,z_j \in \{0,1,\cdots,k-1\}$ 同时 $\sigma_j \in \{\downarrow,\uparrow\}$ 表示自旋向下和向上。这样一种离散化方式共计需要 $k^{3N}\times 2^{N}$ 个数据来表示波函数。在这里，$\mathcal{A}$ 表示反对称化操作（根据泡利不相容原理）并且 $\psi(\mathbf{x_1}, \mathbf{x_2}, \ldots, \mathbf{x_N})=\langle\mathbf{x_1}, \mathbf{x_2}, \ldots, \mathbf{x_N}|\Psi\rangle$。 可以看出，经典计算机存储这样一个波函数需要的内存是随着电子个数呈指数增长的。这使得基于这种离散化的经典数值方法，无法模拟超过几十个电子的系统。那么，我们是不是能够通过量子设备来存储和准备这样一个波函数然后求解基态能量 $E_0$ 呢？在下一节中，我们将以最简单的分子系统 -- 氢分子（$H_2$）为例，讲解 VQE 算法。

**注释2：** 关于量子化学和现有数值计算方法的综述也超过了本教程的处理范围，我们推荐感兴趣的读者去查阅以下经典教材 Helgaker 等人撰写的 *'Molecular Electronic-Structure Theory'* [6] 以及 Szabo & Ostlund 撰写的 *'Modern Quantum Chemistry: Introduction to Advanced Electronic Structure Theory'* [8]。 如果需要弥补量子计算和量子化学之间知识空缺，请参考以下综述文章 [Quantum chemistry in the age of quantum computing](https://pubs.acs.org/doi/10.1021/acs.chemrev.8b00803) [1] 和  [Quantum computational chemistry](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.92.015003) [2] 。

**注释3：** 对于量子化学中的能量计算，我们期望能够达到 **化学精度**（chemical accuracy）$1.6\times10^{-3}$ Ha 或者 1 千卡/摩尔。

## 氢分子 $H_2$ 基态能量

### 构造电子哈密顿量

首先，让我们通过下面几行代码引入必要的 library 和 package。量桨的量子化学工具包是基于 `psi4` 和 `openfermion` 进行开发的，所以需要读者先行安装这两个语言包。在进入下面的教程之前，我们强烈建议您先阅读[哈密顿量的构造](./BuildingMolecule_CN.ipynb)教程，该教程介绍了如何使用量桨的量子化学工具包。

**注意：关于环境设置，请参考 [README_CN.md](https://github.com/PaddlePaddle/Quantum/blob/master/README_CN.md).**

```python
import paddle
import paddle_quantum.qchem as qchem
from paddle_quantum.utils import Hamiltonian
from paddle_quantum.circuit import UAnsatz

import os
import matplotlib.pyplot as plt

import numpy
from numpy import pi as PI
from numpy import savez, zeros

# 无视警告
import warnings
warnings.filterwarnings("ignore")
```

对于具体需要分析的分子，我们需要其**几何构型** (geometry)、**基组**（basis set，例如 STO-3G 基于高斯函数）、**多重度**（multiplicity）以及**分子的净电荷数** (charge) 等多项信息来建模计算出该分子单体积分 (one-body integrations)，双体积分(two-body integrations) 以及哈密顿量等信息。接下来，通过量桨的量子化学工具包将分子的哈密顿量提取出来并储存为 paddle quantum 的 `Hamiltonian` 类，方便我们下一步的操作。

```python
geo = qchem.geometry(structure=[['H', [-0., 0., 0.0]], ['H', [-0., 0., 0.74]]])
# geo = qchem.geometry(file='h2.xyz')

# 将分子信息存储在 molecule 里，包括单体积分（one-body integrations），双体积分（two-body integrations），分子的哈密顿量等
molecule = qchem.get_molecular_data(
    geometry=geo,
    basis='sto-3g',
    charge=0,
    multiplicity=1,
    method="fci",
    if_save=True,
    if_print=True
)
# 提取哈密顿量
molecular_hamiltonian = qchem.spin_hamiltonian(molecule=molecule,
                                               filename=None,
                                               multiplicity=1,
                                               mapping_method = 'jordan_wigner',)
# 打印结果
print("\nThe generated h2 Hamiltonian is \n", molecular_hamiltonian)
```

    FCI energy for H2_sto-3g_singlet (2 electrons) is -1.137283834485513.

    The generated h2 Hamiltonian is
     -0.09706626861762556 I
    -0.04530261550868938 X0, X1, Y2, Y3
    0.04530261550868938 X0, Y1, Y2, X3
    0.04530261550868938 Y0, X1, X2, Y3
    -0.04530261550868938 Y0, Y1, X2, X3
    0.1714128263940239 Z0
    0.16868898168693286 Z0, Z1
    0.12062523481381837 Z0, Z2
    0.16592785032250773 Z0, Z3
    0.17141282639402394 Z1
    0.16592785032250773 Z1, Z2
    0.12062523481381837 Z1, Z3
    -0.2234315367466399 Z2
    0.17441287610651626 Z2, Z3
    -0.2234315367466399 Z3

**注释4：** 生成这个哈密顿量的几何构型中，两个氢原子间的原子间隔（interatomic distance）为 $d = 74$ pm。

除了输入分子的几何结构外，我们还支持读取分子的几何构型文件 (`.xyz` 文件)，关于量子化学工具包更多的用法请参考[哈密顿量的构造](./BuildingMolecule_CN.ipynb)教程。如果你需要测试更多分子的几何构型，请移步至这个[数据库](http://smart.sns.it/molecules/index.html)。

### 搭建量子神经网络（QNN）和试探波函数

在实现VQE的过程中，我们首先需要设计量子神经网络QNN（也可以理解为参数化量子电路）来准备试探波函数 $|\Psi(\boldsymbol\theta)\rangle$。这里，我们提供一个预设好的的深度为 $D$ 层的 4-量子比特的量子电路模板，图中的虚线框内为一层：

- 我们预设一些该参数化电路的参数，比如宽度为 $N = 4$ 量子位。

- 初始化其中的变量参数，${\bf{\theta }}$ 代表我们量子神经网络中的参数组成的向量。

接下来我们根据上图中的电路设计，通过 Paddle Quantum 的 `UAnsatz` 函数和内置的 `real_entangled_layer(theta, D)` 电路模板来高效搭建量子神经网络。

```python
def U_theta(theta, Hamiltonian, N, D):
    """
    Quantum Neural Network
    """

    # 按照量子比特数量/网络宽度初始化量子神经网络
    cir = UAnsatz(N)

    # 内置的 {R_y + CNOT} 电路模板
    cir.real_entangled_layer(theta[:D], D)

    # 铺上最后一列 R_y 旋转门
    for i in range(N):
        cir.ry(theta=theta[D][i][0], which_qubit=i)

    # 量子神经网络作用在默认的初始态 |0000> 上
    fin_state = cir.run_state_vector()

    # 计算给定哈密顿量的期望值
    expectation_val = cir.expecval(Hamiltonian)

    return expectation_val, cir, fin_state
```

### 配置训练模型 - 损失函数

现在我们已经有了数据和量子神经网络的架构，我们将进一步定义训练参数、模型和损失函数。通过作用量子神经网络 $U(\theta)$ 在初始态 $|0..0\rangle$ 上，我们将得到输出态 $\left| {\psi \left( {\bf{\theta }} \right)} \right\rangle $。进一步，在VQE模型中的损失函数一般由量子态 $\left| {\psi \left( {\bf{\theta }} \right)} \right\rangle$ 关于哈密顿量 $H$ 的期望值 (能量期望值 expectation value) 给出，

$$
\min_{\boldsymbol\theta}  \mathcal{L}(\boldsymbol \theta) = \min_{\boldsymbol\theta} \langle \Psi(\boldsymbol\theta)|H |\Psi(\boldsymbol\theta)\rangle
= \min_{\boldsymbol\theta} \sum_k c_k~\langle \Psi(\boldsymbol\theta)| \bigotimes_j \sigma_j^{(k)}|\Psi(\boldsymbol\theta)\rangle.
\tag{8}
$$

```python
class StateNet(paddle.nn.Layer):
    """
    Construct the model net
    """

    def __init__(self, shape, dtype="float64"):
        super(StateNet, self).__init__()

        # 初始化 theta 参数列表，并用 [0, 2*pi] 的均匀分布来填充初始值
        self.theta = self.create_parameter(shape=shape,
                                           default_initializer=paddle.nn.initializer.Uniform(low=0.0, high=2*PI),
                                           dtype=dtype, is_bias=False)

    # 定义损失函数和前向传播机制
    def forward(self, N, D):

        # 计算损失函数/期望值
        loss, cir, fin_state = U_theta(self.theta, molecular_hamiltonian.pauli_str, N, D)

        return loss, cir, fin_state
```

### 配置训练模型 - 模型参数

在进行量子神经网络的训练之前，我们还需要进行一些训练的超参数设置，主要是学习速率（LR, learning rate）、迭代次数（ITR, iteration）和量子神经网络计算模块的深度（D, Depth）。这里我们设定学习速率为 0.5, 迭代次数为 50 次。读者不妨自行调整来直观感受下超参数调整对训练效果的影响。

```python
ITR = 80  # 设置训练的总迭代次数
LR = 0.4   # 设置学习速率
D = 2      # 设置量子神经网络中重复计算模块的深度 Depth
N = molecular_hamiltonian.n_qubits # 设置参与计算的量子比特数
```

### 进行训练

当训练模型的各项参数都设置完成后，我们将数据转化为 Paddle 中的张量，进而进行量子神经网络的训练。过程中我们用的是Adam Optimizer，也可以调用Paddle中提供的其他优化器。我们将训练过程中的结果存储在summary_data文件中。

```python
# 确定网络的参数维度
net = StateNet(shape=[D + 1, N, 1])

# 一般来说，我们利用Adam优化器来获得相对好的收敛，
# 当然你可以改成SGD或者是RMS prop.
opt = paddle.optimizer.Adam(learning_rate=LR, parameters=net.parameters())

# 记录优化结果
summary_iter, summary_loss = [], []

# 优化循环
for itr in range(1, ITR + 1):

    # 前向传播计算损失函数
    loss, cir, fin_state = net(N, D)

    # 在动态图机制下，反向传播极小化损失函数
    loss.backward()
    opt.minimize(loss)
    opt.clear_grad()

    # 更新优化结果
    summary_loss.append(loss.numpy())
    summary_iter.append(itr)

    # 打印结果
    if itr % 20 == 0:
        print("iter:", itr, "loss:", "%.4f" % loss.numpy())
        print("iter:", itr, "Ground state energy:", "%.4f Ha"
                                            % loss.numpy())
    if itr == ITR:
        print("\n训练后的电路：")
        print(cir)

# 储存训练结果到 output 文件夹
os.makedirs("output", exist_ok=True)
savez("./output/summary_data", iter = summary_iter,
                               energy=summary_loss)
```

    iter: 20 loss: -1.0621
    iter: 20 Ground state energy: -1.0621 Ha
    iter: 40 loss: -1.1305
    iter: 40 Ground state energy: -1.1305 Ha
    iter: 60 loss: -1.1358
    iter: 60 Ground state energy: -1.1358 Ha
    iter: 80 loss: -1.1370
    iter: 80 Ground state energy: -1.1370 Ha

    训练后的电路：
    --Ry(4.702)----*--------------x----Ry(4.759)----*--------------x----Ry(0.001)--
                   |              |                 |              |
    --Ry(-1.56)----x----*---------|----Ry(4.698)----x----*---------|----Ry(1.607)--
                        |         |                      |         |
    --Ry(3.170)---------x----*----|----Ry(1.789)---------x----*----|----Ry(4.817)--
                             |    |                           |    |
    --Ry(6.365)--------------x----*----Ry(1.562)--------------x----*----Ry(3.178)--

### 测试效果
我们现在已经完成了量子神经网络的训练，通过 VQE 得到的基态能量的估计值大致为 $E_0 \approx -1.137$ Ha，这与通过全价构型相互作用（FCI）$E_0 = -1.13728$ Ha 计算得出的值是在化学精度 $\varepsilon = 1.6 \times 10^{-3}$ Ha 内相符合的。

```python
result = numpy.load('./output/summary_data.npz')

eig_val, eig_state = numpy.linalg.eig(
                     molecular_hamiltonian.construct_h_matrix())
min_eig_H = numpy.min(eig_val.real)
min_loss = numpy.ones([len(result['iter'])]) * min_eig_H

plt.figure(1)
func1, = plt.plot(result['iter'], result['energy'],
                  alpha=0.7, marker='', linestyle="-", color='r')
func_min, = plt.plot(result['iter'], min_loss,
                  alpha=0.7, marker='', linestyle=":", color='b')
plt.xlabel('Number of iteration')
plt.ylabel('Energy (Ha)')

plt.legend(handles=[
    func1,
    func_min
],
    labels=[
        r'$\left\langle {\psi \left( {\theta } \right)} '
        r'\right|H\left| {\psi \left( {\theta } \right)} \right\rangle $',
        'Ground-state energy',
    ], loc='best')

#plt.savefig("vqe.png", bbox_inches='tight', dpi=300)
plt.show()
```

## 通过 VQE 确定原子间隔

还记得在前面的注释中提到我们默认使用的两个氢原子间原子间隔为 $74$ pm 吗？VQE 的另一个用法便是通过在不同的原子间隔下多次运行然后观察运行结果的最小值是在什么原子间隔发生的，这个间隔即为估计得真实原子间隔。

从上图可以看出，最小值确实发生在 $d = 74$ pm (1 pm = $1\times 10^{-12}$m) 附近，这是与[实验测得数据](https://cccbdb.nist.gov/exp2x.asp?casno=1333740&charge=0)相符合的 $d_{exp} (H_2) = 74.14$ pm.

_______

## 参考文献

[1] Cao, Yudong, et al. Quantum Chemistry in the Age of Quantum Computing. [Chemical reviews 119.19 (2019): 10856-10915.](https://pubs.acs.org/doi/10.1021/acs.chemrev.8b00803)

[2] McArdle, Sam, et al. Quantum computational chemistry. [Reviews of Modern Physics 92.1 (2020): 015003.](https://journals.aps.org/rmp/abstract/10.1103/RevModPhys.92.015003)

[3] Peruzzo, A. et al. A variational eigenvalue solver on a photonic quantum processor. [Nat. Commun. 5, 4213 (2014).](https://www.nature.com/articles/ncomms5213)

[4] Moll, Nikolaj, et al. Quantum optimization using variational algorithms on near-term quantum devices. [Quantum Science and Technology 3.3 (2018): 030503.](https://iopscience.iop.org/article/10.1088/2058-9565/aab822)

[5] 徐光宪, 黎乐民, 王德民. 量子化学: 基本原理和从头计算法(上)[M], 第二版. 北京: 科学出版社, 2012;

[6] Helgaker, Trygve, Poul Jorgensen, and Jeppe Olsen. Molecular electronic-structure theory. John Wiley & Sons, 2014.

[7] Dirac, Paul Adrien Maurice. Quantum mechanics of many-electron systems. [Proceedings of the Royal Society of London. Series A, Containing Papers of a Mathematical and Physical Character 123.792 (1929): 714-733.](https://royalsocietypublishing.org/doi/10.1098/rspa.1929.0094)

[8] Szabo, Attila, and Neil S. Ostlund. Modern quantum chemistry: introduction to advanced electronic structure theory. Courier Corporation, 2012.


# PulseDesigner - Tutorial

`PulseDesigner` is an interactive tool for the construction of pulses utilized to control quantum hardware based on superconducting qubits.

This notebook explains some of the main features of the tool, and how the data can be exported to other quantum hardware-level programming interfaces.

### Running PulseDesigner

To run the tool, execute the [first code cell](#code_cell). This should launch the widget as shown in the image below:

On the top part of the tool, you will find two drop-down menus: one to select the type of signal for the in-phase component, and one for the quadrature component.

The sliders next to each of these menus control each of the signal properties. The slider right below, the number of samples.

The two stack figures show a live plot of the parameters selected with the sliders. All amplitudes are normalized to have a max value of `1`, and the time axis is given in samples (i.e., it is independent of the time resolution of a given backend).

### Example

As an example, consider changing the number of samples to `100`, selecting a `Gaussian` signal for the in-phase component with Sigma of `10` and Amplitude of `0.80`, and a `Gaussian Derivative` signal for the quadrature component, with sigma of `15` and Amplitude of `0.05`. These options should generate the following signal:

To run the tool, execute the code below:

```python
%config InlineBackend.figure_format = 'svg'
from pulsemaker import PulseDesigner
pulse_d = PulseDesigner()
pulse_d
```

### Amplitude Control

An important aspect about the tool is that it guarantees that the norm of the total signal is never greater than `1`:

$$ \sqrt{I^2 + Q^2} \leq 1 $$

This is acheived by readjusting the maximum allowable value of the quadrature signal $Q$ depending on what the value of the in-phase signal $I$ is. So, for example, if we select `Gaussian` signals for both $I$ and $Q$, and set the amplitude of $I$ to `0.8`, the maximum allowable amplitude for $Q$ will be:

$$
\begin{align}
Q_{max} = \sqrt{1 - I^2} = \sqrt{1 - 0.64} =  0.6
\end{align}
$$

If we set the maximum value of $I$ to `1`, the tool will automatically set $Q$ to `0`, and will "gray out" the amplitude slider. To make the slider active again, simply reduce the value of the amplitude for $I$.

Since the `PulseDesigner` is a widget you can bring the same from above right here by executing the next cell.

```python
pulse_d
```

### Exporting to Qiskit

To obtain a pulse waveform that can be interpreted by `qiskit-pulse`, simply use the `.pulse` attribute as shown below:

```python
from qiskit.pulse import library

qiskit_waveform = pulse_d.pulse
qiskit_pulse = library.Waveform(qiskit_waveform, name="Waveform")
qiskit_pulse.draw()
```

```python

```