# Ikeda for many ships

```python
# %load imports.py
"""
These is the standard setup for the notebooks.
"""

%matplotlib inline
%load_ext autoreload
%autoreload 2

from jupyterthemes import jtplot
jtplot.style(theme='onedork', context='notebook', ticks=True, grid=False)

import pandas as pd
pd.options.display.max_rows = 999
pd.options.display.max_columns = 999
pd.set_option("display.max_columns", None)
import numpy as np
import os
import matplotlib.pyplot as plt
from collections import OrderedDict
#plt.style.use('paper')

#import data
import copy
from mdldb.run import Run

from sklearn.pipeline import Pipeline
from rolldecayestimators.transformers import CutTransformer, LowpassFilterDerivatorTransformer, ScaleFactorTransformer, OffsetTransformer
from rolldecayestimators.direct_estimator_cubic import EstimatorQuadraticB, EstimatorCubic
from rolldecayestimators.ikeda_estimator import IkedaQuadraticEstimator
import rolldecayestimators.equations as equations
import rolldecayestimators.lambdas as lambdas
from rolldecayestimators.substitute_dynamic_symbols import lambdify
import rolldecayestimators.symbols as symbols
import sympy as sp

from sympy.physics.vector.printing import vpprint, vlatex
from IPython.display import display, Math, Latex

from sklearn.metrics import r2_score
from src.data import database
from mdldb import tables
import shipflowmotionshelpers.shipflowmotionshelpers as helpers
import src.visualization.visualize as visualize

```

    Duplicate key in file WindowsPath('C:/Users/maa/.matplotlib/stylelib/paper.mplstyle'), line 461 ('figure.figsize   : 5, 3   ## figure size in inches')
    Duplicate key in file WindowsPath('C:/Users/maa/.matplotlib/stylelib/paper.mplstyle'), line 462 ('figure.dpi       : 100        ## figure dots per inch')

```python
from pyscores2.indata import Indata
from pyscores2.runScores2 import Calculation
from pyscores2.output import OutputFile
from pyscores2 import TDPError
import pyscores2
from rolldecayestimators.ikeda import Ikeda, IkedaR
from rolldecayestimators.simplified_ikeda_class import SimplifiedIkeda
import subprocess
from rolldecayestimators import measure
from copy import deepcopy
```

```python
df_all_sections_id = pd.read_csv('../data/external/all_sections.csv', sep=';')
df_all_sections_id.head()
```

      b
      area
      t
      x
      SHIP
      loading_condition_id

      0
      0.00
      0.0
      0.0
      0.00
      68
      NaN

      1
      26.86
      19.5
      6.6
      8.61
      68
      NaN

      2
      28.17
      54.7
      6.6
      17.22
      68
      NaN

      3
      28.40
      96.0
      6.6
      25.83
      68
      NaN

      4
      28.40
      129.4
      6.6
      34.44
      68
      NaN

```python
section_groups=df_all_sections_id.groupby(by='loading_condition_id')
```

```python
loading_condition_ids = df_all_sections_id['loading_condition_id'].unique()
mask=pd.notnull(loading_condition_ids)
loading_condition_ids=loading_condition_ids[mask]
```

```python
df_rolldecay = database.load(rolldecay_table_name='rolldecay_quadratic_b', limit_score=0.99,
                             exclude_table_name='rolldecay_exclude')

df_rolldecay['g'] = 9.81
df_rolldecay['V'] = df_rolldecay['ship_speed']*1.852/3.6
df_rolldecay['Fn'] = df_rolldecay['V']/np.sqrt(df_rolldecay['lpp']*df_rolldecay['g'])
```

```python
mask=df_rolldecay['loading_condition_id'].isin(loading_condition_ids)
df=df_rolldecay.loc[mask].copy()
```

```python
df['BKB'].fillna(0, inplace=True)
```

```python
def create_ikeda(row, indata, output_file, fi_a):

    w = row.omega0
    scale_factor=row.scale_factor
    V = row.ship_speed*1.852/3.6/np.sqrt(scale_factor)
    R = 0.01*row.beam/scale_factor
    lBK=row.BKL/scale_factor
    bBK=row.BKB/scale_factor
    kg = row.kg/scale_factor

    ikeda = Ikeda.load_scoresII(V=V, w=w, fi_a=fi_a, indata=indata, output_file=output_file,
                                scale_factor=scale_factor, BKL=lBK, BKB=bBK, kg=kg, sigma_limit=0.99)

    return ikeda
```

```python
def calculate_ikeda(ikeda):

    output = pd.DataFrame()
    output['B_44_hat']   = ikeda.calculate_B44()
    output['B_W0_hat']   = float(ikeda.calculate_B_W0())
    output['B_W_hat']    = float(ikeda.calculate_B_W())
    output['B_F_hat']    = ikeda.calculate_B_F()
    output['B_E_hat']    = ikeda.calculate_B_E()
    output['B_BK_hat']   = ikeda.calculate_B_BK()
    output['B_L_hat']    = float(ikeda.calculate_B_L())
    output['Bw_div_Bw0'] = float(ikeda.calculate_Bw_div_Bw0())
    return output
```

```python
def get_B_1_B2(s1,s2,fi_as:np.ndarray):

    # Derive linear and quadratic part for all components:

    if isinstance(s1,pd.Series):
        result=pd.Series(name=row.name)
        columns = s1.index
    elif isinstance(s1,pd.DataFrame):
        result=pd.DataFrame()
        columns = s1.columns
    else:
        raise ValueError('s1 must be pd.Series or pd.DataFrame')

    x = fi_as*8*row.omega0/(3 * np.pi)
    B_2 = (s2 - s1) / (x[1] - x[0])
    B_1 = s1 - B_2 * x[0]

    # Save all of the component as one linear term: _1 and a quadratic term: _2

    for key in columns:
        new_name_1 = '%s_1' % key
        result[new_name_1] = s1[key]

        new_name_2 = '%s_2' % key
        result[new_name_2] = s2[key]

    return result
```

```python
results = pd.DataFrame()
fi_as = np.deg2rad([1,10])
ikedas = {}
for run_name, row in df.iterrows():
    loading_condition_id=row['loading_condition_id']

    save_name='%s.in' % row.loading_condition_id
    save_path=os.path.join('scores',save_name)
    indata = Indata()
    if os.path.exists(save_path):
        indata.open(indataPath=save_path)
    else:
        continue

    #calculation = Calculation(outDataDirectory='scores/result')
    output_file_name = 'loading_condition_id_%i.out' % loading_condition_id
    output_file_path = os.path.join(r'scores/result', output_file_name)

    coefficients_file_name = 'loading_condition_id_%i-COEFF.out' % loading_condition_id
    coefficients_file_path = os.path.join(r'scores/result', coefficients_file_name)

    if os.path.exists(coefficients_file_path) and (os.path.getsize(coefficients_file_path) > 0):
        output_file = OutputFile(filePath=output_file_path)
    else:
        continue

    ikeda = create_ikeda(row=row, indata=indata, output_file=output_file, fi_a=fi_as)
    ikedas[run_name] = ikeda
    result_datas = ikeda.calculate()

    # Derive linear and quadratic part for all components:
    s1=result_datas.iloc[0]
    s2=result_datas.iloc[1]
    result = get_B_1_B2(s1=s1, s2=s2, fi_as=fi_as)
    result['id'] = run_name

    results=results.append(result)

```

    c:\python36-64\lib\re.py:212: FutureWarning: split() requires a non-empty pattern match.
      return _compile(pattern, flags).split(string, maxsplit)
    c:\dev\prediction-of-roll-damping-using-fully-nonlinear-potential-flow-and-ikedas-method\venv\lib\site-packages\ipykernel_launcher.py:6: DeprecationWarning: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.

    c:\dev\rolldecay-estimators\rolldecayestimators\ikeda_speed.py:190: RuntimeWarning: divide by zero encountered in true_divide
      So = (0.3 * pi * l * fi_a * f / bBK + 1.95) * bBK;
    c:\dev\rolldecay-estimators\rolldecayestimators\ikeda_speed.py:190: RuntimeWarning: invalid value encountered in multiply
      So = (0.3 * pi * l * fi_a * f / bBK + 1.95) * bBK;
    c:\dev\rolldecay-estimators\rolldecayestimators\ikeda_speed.py:586: RuntimeWarning: invalid value encountered in sqrt
      gamma=sqrt(pi)*f3*(rmax+2*M/H*sqrt(B0**2*A0**2))/((2*Ts*(1-OG/Ts)*sqrt(H0_prim*sigma_prim)))

```python
results.head()
```

      B_44_hat_1
      B_44_hat_2
      B_BK_hat_1
      B_BK_hat_2
      B_E_hat_1
      B_E_hat_2
      B_F_hat_1
      B_F_hat_2
      B_L_hat_1
      B_L_hat_2
      B_W0_hat_1
      B_W0_hat_2
      B_W_hat_1
      B_W_hat_2
      Bw_div_Bw0_1
      Bw_div_Bw0_2
      id

      14848
      0.004183
      0.008973
      0.000490
      0.005105
      0.000005
      0.000048
      0.000591
      0.000722
      0.002860
      0.002860
      0.000034
      0.000034
      0.000238
      0.000238
      6.967885
      6.967885
      14848.0

      2570
      0.001127
      0.005635
      0.000393
      0.004544
      0.000029
      0.000288
      0.000426
      0.000525
      0.000018
      0.000018
      0.000261
      0.000261
      0.000261
      0.000261
      1.000605
      1.000605
      2570.0

      4620
      0.000682
      0.001005
      0.000000
      0.000000
      0.000027
      0.000266
      0.000347
      0.000430
      0.000000
      0.000000
      0.000308
      0.000308
      0.000308
      0.000308
      1.000436
      1.000436
      4620.0

      2573
      0.005406
      0.009703
      0.000407
      0.004519
      0.000006
      0.000064
      0.000553
      0.000680
      0.002474
      0.002474
      0.000259
      0.000259
      0.001966
      0.001966
      7.594869
      7.594869
      2573.0

      2574
      0.006184
      0.010616
      0.000428
      0.004681
      0.000004
      0.000041
      0.000608
      0.000750
      0.003446
      0.003446
      0.000266
      0.000266
      0.001699
      0.001699
      6.376932
      6.376932
      2574.0

## Make comparison with model tests

```python
df2 = df.copy()
scale_factor = df2['scale_factor']
df2['Volume']/=(scale_factor**3)
df2['Disp']=df2['Volume']
df2['beam']/=scale_factor
df2['lpp']/=scale_factor
df2['kg']/=scale_factor
df2['BKB']/=scale_factor
df2['BKL']/=scale_factor
```

```python
df['Disp']=df2['Volume']
results['Disp']=df2.loc[results.index,'Disp']
results['omega0']=df2.loc[results.index,'omega0']

#df_compare = measure.linearized_matrix(df_rolldecay=df2, df_ikeda=results, do_hatify=False)
#mask=df_compare['B_e_hat_ikeda'].notnull()
#df_compare=df_compare.loc[mask].copy()
```

```python
def linearize_model_tests(df_rolldecay, phi_as = np.deg2rad(np.linspace(1,10,10)), g=9.81, rho=1000):

    df = pd.DataFrame()

    for phi_a in phi_as:
        df_ = measure.linearize_model_test(phi_a=phi_a, df_rolldecay=df_rolldecay, g=g, rho=rho)
        df_['phi_a']=phi_a
        df =df.append(df_, ignore_index=True)

    return df

def linearize_ikedas(df_ikeda, phi_as = np.deg2rad(np.linspace(1,10,10)), g=9.81, rho=1000,
                     components = ['B_44_hat', 'B_F_hat', 'B_W_hat', 'B_E_hat', 'B_BK_hat', 'B_L_hat']):

    df = pd.DataFrame()

    for phi_a in phi_as:
        df_ = measure.linearize_si(phi_a=phi_a, df_ikeda=df_ikeda, do_hatify=False, components=components)
        df_['phi_a']=phi_a
        df =df.append(df_, ignore_index=True)

    return df
```

```python
df_rolldecays=linearize_model_tests(df_rolldecay=df2)
df_ikedas=linearize_ikedas(df_ikeda=results)
df_ikedas.dropna(subset=['B_e_hat'], inplace=True)
```

```python
df_rolldecays.describe()
```

      loading_condition_id
      ship_speed
      B_1A
      B_2A
      C_1A
      B_1
      B_2
      C_1
      A_44
      omega0
      omega0_fft
      score
      phi_start
      phi_stop
      id
      project_number
      series_number
      run_number
      test_number
      lcg
      kg
      gm
      CW
      TF
      TA
      BWL
      KXX
      KZZ
      BTT1
      CP
      Volume
      A0
      RH
      scale_factor
      lpp
      beam
      ABULB
      BKX
      TWIN
      DCLR
      VDES
      RHBL
      ASKEG
      PD
      ARH
      PDTDES
      RTYPE
      SFP
      BKL
      BKB
      PROT
      D
      LSKEG
      RR
      XSKEG
      NDES
      AR
      BR
      BRA
      IRUD
      PTYPE
      XRUD
      HSKEG
      RSKEG
      LOA
      ship_type_id
      g
      V
      Fn
      Disp
      B_e
      B_e_hat
      phi_a

      count
      620.000000
      620.000000
      6.200000e+02
      620.000000
      620.000000
      6.200000e+02
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      6.200000e+02
      620.000000
      620.000000
      620.0
      620.000000
      620.000000
      620.000000
      550.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      550.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      580.000000
      620.000000
      620.000000
      620.000000
      620.000000
      550.000000
      620.000000
      0.0
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      580.000000
      620.000000
      620.0
      620.0
      620.000000
      620.0
      620.000000
      620.000000
      620.000000
      620.000000
      570.000000
      620.00
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000
      620.000000

      mean
      49.080645
      10.267742
      1.289510e-01
      0.277129
      8.348334
      1.212530e+01
      28.081467
      864.343979
      103.928647
      2.845734
      2.848816
      0.997662
      -0.026923
      0.001745
      6525.887097
      2.122044e+07
      4.596774
      24.258065
      1.0
      0.151435
      0.288710
      6.150323
      0.849600
      11.311774
      11.311774
      43.798226
      16.088548
      63.020161
      2.506581
      0.772816
      0.763787
      0.987790
      9.456935
      50.853323
      5.088778
      0.864293
      35.262903
      0.253871
      0.629032
      5.742581
      19.068793
      9.929839
      130.559677
      0.890229
      9.824032
      19959.890909
      1.274194
      NaN
      1.439836
      0.007364
      2.387097
      7.727097
      27.874194
      2.344097
      -112.619355
      82.755172
      57.331129
      0.0
      0.0
      1.693548
      1.0
      -130.316694
      7.831935
      21.853226
      271.554355
      3.929825
      9.81
      5.282183
      0.105826
      0.763787
      18.573243
      0.007109
      0.095993

      std
      42.499567
      8.380680
      8.806079e-02
      0.143821
      2.860551
      8.546117e+00
      16.905580
      415.865855
      36.178132
      0.500535
      0.502140
      0.002328
      0.140834
      0.013348
      5230.222655
      4.445701e+06
      4.330676
      21.570739
      0.0
      3.278564
      0.054048
      3.443037
      0.029265
      2.959691
      2.959691
      8.021191
      3.529417
      12.944285
      0.579318
      0.031285
      0.210417
      0.014644
      2.239132
      8.608065
      0.711560
      0.085112
      25.328850
      9.313349
      0.483454
      3.941759
      2.798702
      2.054753
      47.055745
      0.147802
      8.223775
      6377.077880
      1.095878
      NaN
      0.427300
      0.003138
      0.922784
      1.163662
      8.207978
      0.092054
      24.586558
      20.071652
      25.487937
      0.0
      0.0
      0.461392
      0.0
      28.187241
      1.084419
      18.063438
      55.803082
      1.632918
      0.00
      4.311394
      0.085314
      0.210417
      10.124817
      0.003771
      0.050171

      min
      1.000000
      0.000000
      6.451552e-18
      -0.063826
      3.059755
      7.666437e-16
      -4.326756
      35.675407
      5.966548
      1.749216
      1.740442
      0.990496
      -0.156155
      -0.047505
      273.000000
      2.005384e+07
      1.000000
      1.000000
      1.0
      -4.310000
      0.184727
      1.250000
      0.793000
      8.530000
      8.530000
      24.380000
      9.000000
      27.600000
      1.030000
      0.722900
      0.160012
      0.934000
      4.340000
      25.700000
      3.048727
      0.470909
      0.000000
      -11.680000
      0.000000
      0.000000
      10.000000
      5.500000
      44.600000
      0.700000
      0.000000
      2235.000000
      1.000000
      NaN
      0.000000
      0.000000
      1.000000
      4.600000
      13.500000
      2.320000
      -139.500000
      65.100000
      15.500000
      0.0
      0.0
      1.000000
      1.0
      -160.000000
      5.900000
      0.000000
      109.930000
      1.000000
      9.81
      0.000000
      0.000000
      0.160012
      1.960838
      0.000555
      0.017453

      25%
      10.000000
      0.000000
      5.583328e-02
      0.186544
      5.979237
      4.854132e+00
      16.300910
      612.671657
      83.159530
      2.445248
      2.469292
      0.996717
      -0.146740
      -0.005406
      2165.000000
      2.006428e+07
      1.000000
      11.000000
      1.0
      -1.390000
      0.249026
      3.000000
      0.833000
      9.400000
      9.400000
      43.400000
      14.720000
      63.940000
      2.150000
      0.753200
      0.636008
      0.987500
      8.500000
      48.750000
      4.671201
      0.850114
      5.100000
      -4.100000
      0.000000
      0.000000
      19.500000
      9.200000
      108.000000
      0.715000
      0.000000
      16946.000000
      1.000000
      NaN
      1.420000
      0.007443
      1.000000
      7.800000
      24.000000
      2.320000
      -125.000000
      74.500000
      42.600000
      0.0
      0.0
      1.000000
      1.0
      -144.000000
      7.100000
      8.400000
      290.000000
      4.000000
      9.81
      0.000000
      0.000000
      0.636008
      9.472770
      0.003887
      0.052360

      50%
      33.000000
      14.000000
      1.104193e-01
      0.264831
      7.781658
      9.789470e+00
      26.899381
      794.010088
      107.171226
      2.789542
      2.796017
      0.998532
      -0.129595
      0.002573
      4590.000000
      2.009511e+07
      3.000000
      21.500000
      1.0
      -0.216000
      0.267432
      4.900000
      0.848000
      11.500000
      11.500000
      45.800000
      15.500000
      66.200000
      2.440000
      0.766900
      0.777991
      0.990000
      9.500000
      52.088250
      5.245283
      0.868000
      43.900000
      0.000000
      1.000000
      8.400000
      19.800000
      9.800000
      130.000000
      0.871000
      10.415000
      21059.000000
      1.000000
      NaN
      1.563962
      0.007894
      3.000000
      7.950000
      27.500000
      2.320000
      -123.000000
      78.750000
      51.200000
      0.0
      0.0
      2.000000
      1.0
      -142.000000
      7.700000
      14.900000
      290.000000
      4.000000
      9.81
      7.202222
      0.135974
      0.777991
      18.724163
      0.007032
      0.095993

      75%
      99.000000
      18.100000
      2.049045e-01
      0.388284
      11.412434
      1.956510e+01
      38.279906
      1206.009694
      128.956400
      3.378229
      3.380354
      0.999477
      0.143899
      0.009799
      12365.000000
      2.012648e+07
      10.000000
      30.000000
      1.0
      0.620000
      0.334182
      9.900000
      0.869000
      11.900000
      11.900000
      46.970000
      18.790000
      71.000000
      3.000000
      0.785900
      0.864456
      0.994000
      10.400000
      55.000000
      5.627583
      0.930406
      47.500000
      1.000000
      1.000000
      8.400000
      20.350000
      10.900000
      150.000000
      0.992000
      12.800000
      23600.000000
      1.000000
      NaN
      1.675091
      0.008205
      3.000000
      8.300000
      30.000000
      2.320000
      -118.000000
      79.500000
      69.100000
      0.0
      0.0
      2.000000
      1.0
      -139.000000
      8.580000
      31.500000
      295.000000
      4.000000
      9.81
      9.311444
      0.178742
      0.864456
      26.111323
      0.010086
      0.139626

      max
      119.000000
      21.000000
      3.955700e-01
      0.609493
      13.572584
      3.072985e+01
      77.556673
      1675.145698
      194.896832
      3.684099
      3.688230
      0.999853
      0.156054
      0.028980
      15929.000000
      4.010545e+07
      20.000000
      113.000000
      1.0
      10.820000
      0.391440
      11.800000
      0.950000
      22.600000
      22.600000
      60.000000
      22.200000
      80.500000
      3.360000
      0.887500
      1.731909
      0.998600
      15.900000
      71.430000
      5.925808
      0.973105
      113.000000
      36.000000
      1.000000
      10.000000
      21.580000
      16.200000
      258.000000
      1.200000
      46.800000
      28034.000000
      6.000000
      NaN
      1.806973
      0.019091
      3.000000
      10.000000
      49.000000
      2.708000
      -40.500000
      150.000000
      136.400000
      0.0
      0.0
      2.000000
      1.0
      -46.330000
      11.200000
      58.000000
      333.000000
      8.000000
      9.81
      10.803333
      0.238883
      1.731909
      44.549554
      0.016261
      0.174533

```python
df_ikedas.describe()
```

      B_44_hat_1
      B_44_hat_2
      B_BK_hat_1
      B_BK_hat_2
      B_E_hat_1
      B_E_hat_2
      B_F_hat_1
      B_F_hat_2
      B_L_hat_1
      B_L_hat_2
      B_W0_hat_1
      B_W0_hat_2
      B_W_hat_1
      B_W_hat_2
      Bw_div_Bw0_1
      Bw_div_Bw0_2
      id
      Disp
      omega0
      B_44_hat_e
      B_F_hat_e
      B_W_hat_e
      B_E_hat_e
      B_BK_hat_e
      B_L_hat_e
      B_e_hat
      phi_a

      count
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000
      500.000000

      mean
      0.004222
      0.008002
      0.000436
      0.003867
      0.000026
      0.000260
      0.000506
      0.000621
      0.001456
      0.001456
      0.000604
      0.000604
      0.001798
      0.001798
      3.613436
      3.613436
      5759.000000
      0.762174
      2.862443
      0.006139
      0.000650
      0.002264
      0.000091
      0.001362
      0.001772
      0.006139
      0.095993

      std
      0.002688
      0.003375
      0.000537
      0.001731
      0.000025
      0.000253
      0.000101
      0.000122
      0.001475
      0.001475
      0.000553
      0.000553
      0.001786
      0.001786
      2.581554
      2.581554
      4942.594039
      0.170836
      0.486015
      0.003692
      0.000152
      0.002327
      0.000107
      0.001015
      0.001773
      0.003692
      0.050181

      min
      0.000610
      0.001005
      0.000000
      0.000000
      0.000001
      0.000010
      0.000305
      0.000372
      0.000000
      0.000000
      0.000034
      0.000034
      0.000034
      0.000034
      1.000127
      1.000127
      273.000000
      0.160012
      2.118674
      0.000714
      0.000320
      0.000035
      0.000001
      0.000000
      0.000000
      0.000714
      0.017453

      25%
      0.001965
      0.005635
      0.000316
      0.002698
      0.000006
      0.000064
      0.000426
      0.000525
      0.000000
      0.000000
      0.000132
      0.000132
      0.000312
      0.000312
      1.000436
      1.000436
      2164.000000
      0.674625
      2.445248
      0.002929
      0.000539
      0.000374
      0.000018
      0.000725
      0.000000
      0.002929
      0.052360

      50%
      0.004450
      0.008143
      0.000398
      0.004478
      0.000019
      0.000188
      0.000525
      0.000641
      0.001236
      0.001236
      0.000310
      0.000310
      0.001155
      0.001155
      3.498247
      3.498247
      3358.500000
      0.786357
      2.777860
      0.005958
      0.000643
      0.001448
      0.000055
      0.001195
      0.001504
      0.005958
      0.095993

      75%
      0.006317
      0.010605
      0.000475
      0.004845
      0.000034
      0.000345
      0.000583
      0.000722
      0.002417
      0.002417
      0.001156
      0.001156
      0.002952
      0.002952
      5.418093
      5.418093
      8701.000000
      0.864456
      3.375072
      0.008799
      0.000754
      0.003513
      0.000120
      0.001846
      0.002886
      0.008799
      0.139626

      max
      0.011756
      0.017916
      0.004059
      0.010099
      0.000118
      0.001178
      0.000791
      0.000902
      0.005772
      0.005772
      0.001544
      0.001544
      0.005635
      0.005635
      11.618038
      11.618038
      15929.000000
      1.063959
      3.684099
      0.018246
      0.001118
      0.008618
      0.000738
      0.007718
      0.007606
      0.018246
      0.174533

```python
df_compare = pd.merge(left=df_rolldecays, right=df_ikedas, how='inner', left_on=('id','phi_a'), right_on=('id','phi_a'), suffixes=('','_ikeda'))
```

```python
fig,ax=plt.subplots()
df_compare.plot(x='B_e_hat', y='B_e_hat_ikeda', ax=ax, style='o', alpha=0.30, ms=10, markeredgewidth=0)

xlim = ax.get_xlim()
ylim = ax.get_ylim()
lim = np.max([xlim[1],ylim[1]])
ax.set_xlim(0,lim)
ax.set_ylim(0,lim)
ax.plot([0,lim],[0,lim],'r-')

ax.grid(True)
ax.set_aspect('equal', 'box')
ax.get_legend().remove()
```

```python
mask = df_compare['ship_speed'] < 1
df_compare_zero_speed = df_compare.loc[mask]
df_compare_speed = df_compare.loc[~mask]

```

```python
fig,ax=plt.subplots()
df_compare_zero_speed.plot(x='B_e_hat', y='B_e_hat_ikeda', ax=ax, style='o', alpha=0.30, ms=10, markeredgewidth=0)

ax.set_xlabel(r'$\hat{B_{e}}^{Model}$')
ax.set_ylabel(r'$\hat{B_{e}}^{Ikeda}$')

xlim = ax.get_xlim()
ylim = ax.get_ylim()
lim = np.max([xlim[1],ylim[1]])
ax.set_xlim(0,lim)
ax.set_ylim(0,lim)
ax.plot([0,lim],[0,lim],'r-')

ax.grid(True)
ax.set_aspect('equal', 'box')
ax.get_legend().remove()
```

```python
fig,ax=plt.subplots()
df_compare_speed.plot(x='B_e_hat', y='B_e_hat_ikeda', ax=ax, style='o', alpha=0.30, ms=10, markeredgewidth=0)

ax.set_xlabel(r'$\hat{B_{e}}^{Model}$')
ax.set_ylabel(r'$\hat{B_{e}}^{Ikeda}$')

xlim = ax.get_xlim()
ylim = ax.get_ylim()
lim = np.max([xlim[1],ylim[1]])
ax.set_xlim(0,lim)
ax.set_ylim(0,lim)
ax.plot([0,lim],[0,lim],'r-')

ax.grid(True)
ax.set_aspect('equal', 'box')
ax.get_legend().remove()
```

```python
fig,ax=plt.subplots()

mask_bilge_keel = df_compare_zero_speed.BKL > 0

A0 = df_compare_zero_speed['A0']
bins = np.linspace(A0.min(),A0.max(),20)

df_compare_zero_speed.loc[mask_bilge_keel].hist('A0', bins=bins, ax=ax, label='bilge keel', color='green')
df_compare_zero_speed.loc[~mask_bilge_keel].hist('A0', bins=bins, ax=ax, label='no bilge keel', color='red')

ax.annotate('limit', xy=(0.99,0), xytext=(0.99,5), arrowprops=dict(facecolor='black', shrink=0.05))
ax.legend()
```

```python
fig,ax=plt.subplots()
for run_id,df_ in df_compare_zero_speed.loc[mask_bilge_keel].groupby(by='id'):
    df_.plot(x='B_e_hat', y='B_e_hat_ikeda', ax=ax, style='.-', alpha=0.30)

for run_id,df_ in df_compare_zero_speed.loc[~mask_bilge_keel].groupby(by='id'):
    df_.plot(x='B_e_hat', y='B_e_hat_ikeda', ax=ax, style='ro', alpha=0.70, ms=10, markeredgewidth=0)

ax.set_xlabel(r'$\hat{B_{e}}^{Model}$')
ax.set_ylabel(r'$\hat{B_{e}}^{Ikeda}$')

xlim = ax.get_xlim()
ylim = ax.get_ylim()
lim = np.max([xlim[1],ylim[1]])
ax.set_xlim(0,lim)
ax.set_ylim(0,lim)
ax.plot([0,lim],[0,lim],'r-')

ax.grid(True)
ax.set_aspect('equal', 'box')
ax.get_legend().remove()
```

```python
df_ = df_compare_zero_speed.loc[mask_bilge_keel]
id=(df_['B_e_hat'] - df_['B_e_hat_ikeda']).abs().sort_values(ascending=False).index[0]
run_id = df_compare_zero_speed.loc[id]['id']
```

```python
run_ids = df_compare['id'].unique()
df_ = df_rolldecay.loc[run_ids]
df_speed_variations = df_.groupby(by = 'loading_condition_id').filter(lambda x: len(x['ship_speed'].unique()) > 3)
loading_conditions = df_speed_variations.groupby(by = 'loading_condition_id').first()
#loading_conditions
```

```python
loading_condition_ids = [34,107]

ymaxs = []
axess = []
for loading_condition_id in loading_condition_ids:

    mask = df_rolldecay.loading_condition_id == loading_condition_id
    df_runs = df_rolldecay.loc[mask]

    run_groups = df_runs.groupby(by='Fn')

    fig,axes=plt.subplots(ncols=len(run_groups))
    fig.set_size_inches(30,8)

    ## Amplitude variation:
    for (Fn, df),ax in zip(run_groups,axes):

        axess.append(ax)

        run_id = df.iloc[0]['id']
        ikeda = deepcopy(ikedas[run_id])
        phi_as = np.deg2rad(np.linspace(0,10,100))

        result = ikeda.calculate(fi_a=phi_as)
        result['phi_a'] = phi_as
        result.set_index('phi_a', inplace=True)

        visualize.plot_area(result, ax=ax, interesting_ = ['B_W_hat', 'B_F_hat', 'B_E_hat', 'B_L_hat', 'B_BK_hat'])
        ax.set_title('Fn:%0.2f' % Fn)

        ylims = ax.get_ylim()
        ymaxs.append(ylims[1])

ymax=np.max(ymaxs)
for ax in axess:
    ax.set_ylim((0,ymax))

ymaxs = []
axess = []
for loading_condition_id in loading_condition_ids:

    mask = df_rolldecay.loading_condition_id == loading_condition_id
    run_id = df_rolldecay.loc[mask].index[0]

    ## Speed variation:
    ikeda = deepcopy(ikedas[run_id])
    phi_as = np.deg2rad([10])
    V = np.linspace(df_runs['V'].min(), df_runs['V'].max(), 100)
    V/=np.sqrt(ikeda.scale_factor)
    g = 9.81
    Fn = V/np.sqrt(ikeda.lpp*g)

    ikeda.V = V
    #ikeda.bBK = ikeda.bBK[0]*np.ones(len(V)).T
    #ikeda.lBK = ikeda.lBK[0]*np.ones(len(V)).T

    result = ikeda.calculate(fi_a=phi_as)
    result['Fn'] = Fn
    result.set_index('Fn', inplace=True)
    fig,ax=plt.subplots()
    axess.append(ax)
    visualize.plot_area(result, ax=ax, interesting_ = ['B_W_hat', 'B_F_hat', 'B_E_hat', 'B_L_hat', 'B_BK_hat'])

    ylims = ax.get_ylim()
    ymaxs.append(ylims[1])

ymax=np.max(ymaxs)
for ax in axess:
    ax.set_ylim((0,ymax))

```

```python
print(df_runs.iloc[0].project_path)
```

```python
df_speed_variations = df_rolldecay.groupby(by = 'loading_condition_id').filter(lambda x: len(x) > 3)
```

```python
loading_conditions = df_speed_variations.groupby(by = 'loading_condition_id').first()
```

```python
#print(loading_conditions.loc[95].project_path)
```

```python

```


```python
# Run the below command to install kanren.
# It does not come pre-installed with the Anaconda distribution of Jupyter Notebook. You will see an installation like below.
```

```python
pip install kanren
```

    Collecting kanren
      Downloading kanren-0.2.3.tar.gz (23 kB)
    Requirement already satisfied: toolz in /usr/local/lib/python3.7/dist-packages (from kanren) (0.11.1)
    Collecting multipledispatch
      Downloading multipledispatch-0.6.0-py3-none-any.whl (11 kB)
    Collecting unification
      Downloading unification-0.2.2-py2.py3-none-any.whl (10 kB)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from multipledispatch->kanren) (1.15.0)
    Building wheels for collected packages: kanren
      Building wheel for kanren (setup.py) ... done
      Created wheel for kanren: filename=kanren-0.2.3-py3-none-any.whl size=15871 sha256=408e9a8fb9b926671951ce9433721cf28699aa23187edae2dfd1691c807a1904
      Stored in directory: /root/.cache/pip/wheels/26/cb/32/093b860ec60752641d76d706af25a175597cf56e152282cbbe
    Successfully built kanren
    Installing collected packages: multipledispatch, unification, kanren
    Successfully installed kanren-0.2.3 multipledispatch-0.6.0 unification-0.2.2

```python
# Once kanren is installed to the Jupyter Notebook kernel, run the below command to install sympy.
# All required components of the sympy collection will get installed
```

```python
pip install sympy
```

    Requirement already satisfied: sympy in /usr/local/lib/python3.7/dist-packages (1.7.1)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy) (1.2.1)

**Evaluating Mathematical Idioms using Logic Programming**

Algorithms are nothing but implementation of logic and control. Similarly, when the logic runs a mathematical function, we call it a mathematical expression. These expressions are the inputs we give to the program, based on which the program understands the rules that are present in the logic. Based on the understanding of these rules, future expressions can also be evaluated. Let us see an implementation of Logic Programming to evaluate mathematical expressions:

```python
# Import the necessary functions from the kanren library
from kanren import run, var, fact
from kanren.assoccomm import eq_assoccomm as eq
from kanren.assoccomm import commutative, associative

# Define values that will undertake the addition and multiplication operations
addition = 'add'
multiplication = 'mul'

# Define facts and properties of each operation
fact(commutative, multiplication)
fact(commutative, addition)
fact(associative, multiplication)
fact(associative, addition)

# Declare the variables that are going to form the expression
var_x, var_y, var_z = var('var_x'), var('var_y'), var('var_z')

# Build the correct pattern that the program needs to learn
match_pattern = (addition, (multiplication, 4, var_x, var_y), var_y, (multiplication, 6, var_z))
match_pattern = (addition, (multiplication, 3, 4), (multiplication, (addition, 1, (multiplication, 2, 4)),2))

# Build 3 distinct expressions to test if the function has learnt
test_expression_one = (addition, (multiplication, (addition, 1 , (multiplication, 2, var_x )), var_y) ,(multiplication, 3, var_z ))
test_expression_two = (addition, (multiplication, var_z, 3), (multiplication, var_y, (addition, (multiplication, 2, var_x), 1)))
test_expression_three = (addition  , (addition, (multiplication, (multiplication, 2, var_x), var_y), var_y), (multiplication, 3, var_z))
```

```python
# Test the evaluations of the expression on the test expressions
run(0,(var_x,var_y,var_z),eq(test_expression_one,match_pattern))
```

    ((4, 2, 4),)

```python
run(0,(var_x,var_y,var_z),eq(test_expression_two,match_pattern))
```

    ((4, 2, 4),)

```python
print(run(0,(var_x,var_y,var_z),eq(test_expression_three,match_pattern)))
```

    ()

```python
# Since the first two expressions satisfy the expression above, they return the values of individual variables.
# The third expression is structurally different and therefore does not match.
```

```python
# Running Mathematical Evaluations using SymPy
import math
import sympy

print (math.sqrt(8))
```

    2.8284271247461903

```python
# Although the Math Square Root function gives an output for the Square Root of 8.
# We know this is not accurate since the square root of 8 is a recursive, non-ending real number
print (sympy.sqrt(3))
```

    sqrt(3)

```python
# Sympy on the other hand, symbolizes the output and shows it as root of 3
# In case of actual square roots like 9, SymPy gives the correct result and not a symbolic answer
```

```python
# Import the necessary libraries for running the Prime Number function
from kanren import membero, isvar, run
from kanren.core import goaleval, condeseq, success, fail, eq, var
from sympy.ntheory.generate import isprime, prime
import itertools as iter_one

# Defining a function to build the expression
def exp_prime (input_num):
  if isvar(input_num):
    return condeseq([(eq, input_num, x)] for x in map(prime, iter_one.count(1)))
  else:
      return success if isprime (input_num) else fail
```

```python
# Variable to use
n_test = var()
set(run(0, n_test,(membero, n_test,(12,14,15,19,21,20,22,29,23,30,41,44,62,52,65,85)),( exp_prime, n_test)))
```

    {19, 23, 29, 41}

```python
run(7, n_test, exp_prime( n_test ) )
```

    (2, 3, 5, 7, 11, 13, 17)

```python

```

03. Logic Programming for AI/Code/Logic_Programming_for_AI.ipynb


# Ex 3: Manipulating Spectra Part 2 - `Spectra`

## Notebook Initialization

```python
%load_ext autoreload
import sys
sys.path.append("..")
```

```python
%matplotlib inline
```

```python
%autoreload

import matplotlib
matplotlib.rc_file('matplotlibrc')
import matplotlib.pyplot as plt

import numpy as np
from scipy.integrate import quad

import darkhistory.physics as phys
import darkhistory.utilities as utils
import darkhistory.spec.spectools as spectools
from darkhistory.spec.spectrum import Spectrum
from darkhistory.spec.spectra import Spectra
```

## `Spectra` Class - Introduction
---------------------------------------------

The [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) class is simply a collection of [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects. The main use of [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) is as a list of spectra that indexes spectra at different redshifts (e.g. the low-energy photon spectrum deposited at each redshift) or at different injection energies (e.g. the spectrum of photons produced by injecting an electron at different energies).

To demonstrate how to use this class, consider the extragalactic photon flux coming from the smooth distribution of dark matter throughout the universe, decaying with some spectrum $dN/dE'$. The $'$ notation is used to indicate the spectrum *at production*. For a delta function photon spectrum from the decay, $dN/dE' = 2 \delta(E' - m_{\text{DM}}/2)$, the photon flux (i.e. the number of photons received per area per energy per steradian) can be evaluated exactly as

$$ \frac{d^2 \Phi}{d\Omega \, dE} = \frac{1}{4\pi} \frac{\Gamma \Omega_{\text{DM}} \rho_c}{m_{\text{DM}} H_0 \sqrt{\Omega_m}} \cdot \frac{4}{m_{\text{DM}}} \sqrt{\frac{2E}{m_{\text{DM}}}} \frac{\Theta(m_{\text{DM}}/2 - E)}{\sqrt{1 + \kappa(2E/m_{\text{DM}})^3}} $$

where $\Gamma$ is the decay width, $\Omega_\text{DM} \rho_c$ and $\Omega_m \rho_c$ are the energy density of dark matter and all matter today, $\rho_c$ is the critical density today, $H_0$ is the Hubble constant, and $\kappa \equiv \Omega_\Lambda/\Omega_m \sim 3$ is the ratio of the energy densities of dark energy to all matter. $\Theta(x)$ is the usual step function. Instead of evaluating this analytically, we will now instead compute a discrete approximation, and see that we recover some approximation of the final analytic result.

The main attributes of the [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) class are very similar to those of [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html), and are as follows:

1. `eng`, length *M:* the energy abscissa of the spectra. All spectra stored within a single [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) class must have the same abscissae;

2. `in_eng`, length *N:* the injection energy abscissa of the spectra. This should be initialized appropriately if the spectra correspond to secondary spectra of injected particles at different *injection energies*; otherwise, it is initialized to an array of -1;

3. `rs`, length *N:* the redshift abscissa of the spectra. This should be initialized appropriately if the spectra corresponds to the same kind of output at different *redshifts*; otherwise, it is initialized to an array of -1;

4. `grid_vals`, shape *(N,M)*: the 2D grid storing all of the data. The first dimension corresponds to either `in_eng` or `rs`, while the second dimension is `eng`;

5. `spec_type`, which can be either `'N'` or `'dNdE'`, and determines how the data is stored in `grid_vals`, and

6. `N_underflow` and `eng_underflow`, length *N*, to store the underflow in number and energy.

To begin, we will initialize a list of [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects, each containing a delta function at half the dark matter mass, taken to be $m_{\text{DM}}$ = 1 GeV. This can be efficiently done by [*rebin_N_arr()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectools/darkhistory.spec.spectools.rebin_N_arr.html), which takes a list of energies (second input), a list containing the number of particles at that energy (first input), and an output energy abscissa (third input), and returns a [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) object with the output energy abscissa and spectrum approximating the initial particle input, conserving both total number and total energy.

```python
# We'll work in GeV for the this part of the code.
mDM = 1

# Energy and redshift abscissa.
# Redshift is ordered from largest to smallest.
eng = 10**np.arange(-3, 0, 0.05)
rs_arr  = 10**np.arange(3, 0, -0.003)

# Create the list of Spectrum objects.
spec_arr = [
    spectools.rebin_N_arr(np.array([2]), np.array([mDM/2]), eng)
    for rs in rs_arr
]

# Assign the correct redshift to each of the Spectrum objects.
# The first Spectrum object is assigned the largest redshift.
for spec,rs in zip(spec_arr,rs_arr):
    spec.rs = rs
```

We are now ready to use the [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) class. Given a list of [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects with identical energy abscissa, we can create a [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object as follows:

```python
photon_specs = Spectra(spec_arr)
```

A [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object can also be directly initialized from a 2D array. Here, we initialize with a 3x4 matrix, with appropriate entries for *rs*, *in_eng* and *eng*:

```python
    test_arr = np.ones_like((3, 4))
    test_spectra = Spectra(
        test_arr,
        eng=np.array([1, 10, 100, 1000]),  # must be length 4
        in_eng=np.array([3, 30, 300])      # must be length 3
    )
    # test_spectra.rs will be automatically initialized to array([-1, -1, -1]).
    # test_spectra.grid_vals == test_arr will be True.
```

Initializing with a collection of [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects which have been correctly initialized will correctly populate *rs*, *in_eng* and *eng* from these objects.

A few simple functions exist to get attributes from the [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects stored in a [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object. The user can run the following lines to inspect these attributes:

```python
print('The energy abscissa of each Spectrum in photon_specs is: ')
print(photon_specs.eng)
print('*******************************************************')
print('The redshift of each Spectrum in photon_specs is: ')
print(photon_specs.rs)
print('*******************************************************')
print('The injection energy of each Spectrum in photon_specs is: ')
print(photon_specs.in_eng)
print('*******************************************************')
print('The 3rd Spectrum in photon_specs has N values (should add up to 2): ')
print(photon_specs[2].N)
print('*******************************************************')
```

## Basic Operations

At this point we simply have a list of spectra with a delta-function spectrum at $m_\text{DM}/2$, produced at different redshifts. To turn this into a flux that is observed on Earth, we need to do some cosmology.

The spectrum of photons produced per volume per time, is given by

$$ \frac{dN}{dV' dt' dE'} = \frac{dN}{dE'} \Gamma n_{\chi,0}(1+z')^3 $$

where $n_{\chi,0}$ is the dark matter number density today. To convert this into a flux observed *today*, we need to relate the $'$ variables to the present day. We have $(1+z')^3 dV' = dV$ from the expansion, and $dE' = (1+z') dE$ from the redshifting of photons. Thus the flux observed today is

$$
\begin{align}
\frac{d^2 \Phi}{d\Omega dE} &= \frac{1}{4\pi}  \int_0^\infty dt' \left. \frac{dN}{dV' \, dt' \, dE'} \right|_{E' = E(1+z)} \frac{dV'}{dV} \frac{dE'}{dE} \\
&= \frac{\Gamma n_{\chi,0}}{4\pi} \int_0^\infty d \log(1+z') \frac{dt'}{d \log(1+z')} \left. \frac{dN}{dE'} \right|_{E' = E(1+z')} (1+z') \\
&= \frac{ \Gamma n_{\chi,0}}{4\pi H_0 \sqrt{\Omega_m}} \int_0^\infty d \log(1+z') \left. \frac{dN}{dE'} \right|_{E' = E(1+z')} \frac{1}{\sqrt{1+z'} \sqrt{1 + \kappa(1+z')^{-3}}}
\end{align}
$$

We can now obtain the integral numerically. First, note that the spectrum

$$ \left. \frac{dN}{dE'} \right|_{E' = E(1+z')} = 2 \delta(E(1+z') - m_\text{DM}/2)$$

is simply all of the delta-function spectra in `photon_specs` redshifted from their starting redshifts (which are tabulated in `rs_arr`) down to $z = 0$, and then divided by $1+z'$. You can check that this extra factor is necessary based on the normalization of each spectrum (redshifting of spectra preserves total number in DarkHistory, and only shifts energies).

We can then treat all of these spectra as approximating the produced spectrum within some bin of width $\Delta \log(1+z')$:

$$
\frac{d^2 \Phi}{d \Omega dE} \approx \frac{\Gamma n_{\chi,0}}{4 \pi H_0 \sqrt{\Omega_m}} \sum_i \left.\frac{dN}{dE'}\right|_{E'=E(1+z_i')} \frac{\Delta \log(1+z_i')}{\sqrt{1+z_i'} \sqrt{1 + \kappa(1+z_i')^{-3}}}
$$

First, let's perform the redshifting. This can be done with [*Spectra.redshift()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.redshift), which takes a list of floats to redshift to.

```python
photon_specs.redshift(np.ones_like(photon_specs.rs))
```

And then we divide by the redshift value for the correct normalization. Note that you can manipulate [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) very naturally with standard mathematical operations.

```python
photon_specs /= rs_arr
```

Next, each of these spectra must be multiplied by a factor of

$$ \frac{\Delta \log(1+z_i)}{\sqrt{1+z_i} \sqrt{1 + \kappa(1 + z_i)^{-3}}} $$

To find $\Delta \log z_i$, we use the convenience function [*spectools.get_log_bin_width()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectools/darkhistory.spec.spectools.get_log_bin_width.html), which takes a redshift array and computes the log bin width of each bin. Note the minus sign, since our redshifts are currently stored in descending order.

```python
dlogz = -spectools.get_log_bin_width(rs_arr)
```

We can now simply compute an array of this factor, and then multiply `photon_specs` by it:

```python
kappa = phys.omega_lambda/phys.omega_m

fac = dlogz/(np.sqrt(rs_arr)*np.sqrt(1 + kappa*rs_arr**(-3)))
photon_specs *= fac
```

We can also multiply in the remaining prefactors (here we choose $\Gamma$ to be in natural units):

```python
# This choice of the width (in GeV) is roughly a lifetime of 1e25 seconds.
Gamma = 1e-50

prefac = (
    (1/(4*np.pi)) * Gamma * phys.rho_DM/(mDM*1e9)
    / (phys.H0 * np.sqrt(phys.omega_m))
    )*phys.c/(phys.hbar*1e-9)

photon_specs *= prefac
```

Note that all units of fundamental parameters in [*physics*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/darkhistory.physics.html) are given in eV (mass, energy and temperature), cm and seconds, and the final value of the photon flux has been multiplied by appropriate numerical factors to give `photon_specs` units of $\text{cm}^{-2} \text{ GeV}^{-1} \text{ s}^{-1}$.

## Summing of Spectra

The last remaining thing to do is to perform the integral by summing up the discrete spectra. This can be done using [*Spectra.sum_specs()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.sum_specs), which returns a [*Spectrum*](https://darkhistory.readthedocs.io/en/development/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) object:

```python
obs_photon_flux = photon_specs.sum_specs()
```

Now to compare this with the analytic expression.

```python
def analytic_flux(eng):
    factor = 4/mDM*np.sqrt(2*eng/mDM)*1/np.sqrt(1 + kappa*(2*eng/mDM)**3)
    factor[eng > mDM/2] = 0
    return np.array(prefac*factor)
```

```python
plt.figure()

plt.loglog()

discrete_spec, = plt.plot(
    obs_photon_flux.eng, obs_photon_flux.dNdE, label='Discrete Spectrum'
)

dNdE_analytic = analytic_flux(obs_photon_flux.eng)

analytic_spec, = plt.plot(
    obs_photon_flux.eng, dNdE_analytic, 'go', marker='o', markersize='6',
    markerfacecolor='w', markevery=2,
    label='Analytic Spectrum'
)

plt.legend(handles=[discrete_spec, analytic_spec])

plt.title('Extragalactic Photon Flux')
plt.xlabel('Photon Energy [GeV]')
plt.ylabel(r'$d^2\Phi/d\Omega\, dE$ [cm$^{-2}$ GeV$^{-1}$ sr$^{-1}$ s$^{-1}$]')
plt.axis([1e-3, 1, 1e-6, 3e-4])
```

[*Spectra.sum_specs()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.sum_specs) can also be called with an argument specifying some weight to be assigned to each of the spectra. So the preceding analysis could have been done using the following code:

```python
# Create the list of Spectrum objects.
spec_arr = [
    spectools.rebin_N_arr(np.array([2/rs]), np.array([mDM/2]), eng)
    for rs in rs_arr
           ]

# Assign the correct redshift to each of the Spectrum objects.
# The first Spectrum object is assigned the largest redshift.
for spec,rs in zip(spec_arr,rs_arr):
    spec.rs = rs
    # Redshifting
    spec.redshift(1)

photon_flux_2 = Spectra(spec_arr)
# Weighted sum
obs_photon_flux_2 = photon_flux_2.sum_specs(fac*prefac)
```

## Interpolation and Plotting

The two key advantages of using the  object is interpolation and plotting. Given a [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object with spectra corresponding to different redshifts, [*Spectra.at_rs()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.at_rs) takes in an array of new redshifts, and performs a linear interpolation over the whole [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) to give you a new [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object, with spectra at the new redshifts.

To demonstrate this, we return to the CMB energy spectrum once again, and generate a [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object storing the number of CMB photons per unit energy per co-moving volume at several different redshifts. We do this by initializing the [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object with an empty array, and then using [*Spectra.append()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.append) to add more [*Spectrum*](https://darkhistory.readthedocs.io/en/development/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html) objects to it.

```python
# Define the output energy abscissa.
eng = 10**((np.arange(120)-90)*(1/10))
# Define the redshift abscissa.
rs_arr = 1+np.arange(100)

# Analytic CMB spectrum

CMB_specs = Spectra([])
for rs in rs_arr:
    spec = spectools.discretize(eng, phys.CMB_spec, phys.TCMB(1)*rs)
    # Change the result to per co-moving volume
    spec *= rs**3
    spec.rs = rs
    CMB_specs.append(spec)
```

We can interpolate to get the spectrum at any intermediate redshift within the abscissa of the [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object, using [*Spectra.at_rs()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.at_rs). This returns a new [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object with just the spectra at the specified redshift. The interpolation is done in log redshift.

```python
rs_selected = np.array([2.495, 30.4903, 99.910])
CMB_specs_selected = CMB_specs.at_rs(rs_selected)
print('CMB_specs_selected has the following redshift abscissa: ',
     CMB_specs_selected.rs)
```

Let's plot the result. [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) includes a function [*Spectra.plot()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html), meant to make plotting of multiple spectra at the same time easy. The axis of the plot is passed to the function, and keyword arguments to `matplotlib` can simply be passed to the function to change the plot properties.

For a start, we call [*Spectra.plot()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) for `CMB_specs_selected` by passing just the axis and keyword arguments for plotting: this simply plots every spectrum inside it (**NB: Doing this for a large [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) object will take a long time!**). We overlay the analytic result for comparison.

```python
analytic_spec = [
    phys.CMB_spec(
        CMB_specs_selected.eng, phys.TCMB(1)*rs
    )*rs**3 for rs in rs_selected
]

ax = plt.subplot(1,1,1)
plt.rc('text', usetex=True)

spectra_plot = CMB_specs_selected.plot(ax, linewidth=2)

plt.gca().set_prop_cycle(None)

analytic_plot = [
    plt.plot(
        CMB_specs_selected.eng, spec, 'o',
        marker='o', markersize='6', markevery=3, markerfacecolor='w'
    ) for spec in analytic_spec
]

plt.legend(spectra_plot, ('1+z = 2.495','1+z = 30.4903', '1+z = 99.91'))

ax.set_xscale('log')
ax.set_yscale('log')

plt.title('CMB Spectrum')
plt.xlabel('Photon Energy [eV]')
plt.ylabel(r'$dn_\gamma/dE$ [eV$^{-1}$ cm$^{-3}$]')
plt.axis([1e-8, 100, 1e-7, 1e17])
```

[*Spectra.plot()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) can be used to plot selected spectra, for example at arbitrary redshifts within the redshift abscissa. Linear interpolation is done automatically to pick out the correct spectra.

[*Spectra.plot()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) also takes the argument `fac`, which allows the user to multiply the spectra by some arbitrary factor.

```python
plt.figure()

plt.loglog()

ax = plt.gca()

rs_to_plot = np.array([90, 45, 22.5, 11.25])
spectra_plot = CMB_specs.plot(ax, ind=rs_to_plot, indtype='rs', fac=CMB_specs.eng**2, linewidth=2)

plt.legend(spectra_plot, ('1+z = 90','1+z = 45', '1+z = 22.5', '1+z = 11.25'))

plt.gca().set_prop_cycle(None)

analytic_spec = [
    phys.CMB_spec(
        CMB_specs.eng, phys.TCMB(1)*rs
    )*CMB_specs.eng**2*rs**3 for rs in rs_to_plot
]

analytic_plot = [
    plt.plot(
        CMB_specs_selected.eng, spec, 'o',
        marker='o', markersize='6', markevery=3, markerfacecolor='w'
    ) for spec in analytic_spec
]

plt.title('CMB Spectrum')
plt.xlabel('Photon Energy [eV]')
plt.ylabel(r'$E^2 \, dn_\gamma/dE$ [eV cm$^{-3}$]')
plt.axis([1e-8, 100, 1e-7, 1e17])
```

We can also make plots without interpolation, by selecting the spectra stored in the `Spectra` object by specifying the index. There are many different ways to specify what to plot, and the user should refer to the documentation to find out more. Below, we plot the spectra between two indices in steps:

```python
plt.figure()

ax = plt.gca()
ax.loglog()

# Plots index 20 to 99, steps of 10 (i.e. 20, 30, 40, 50, 60, 70, 80, 90)
spectra_plot = CMB_specs.plot(ax, ind=(20,100), step=10, indtype='ind', linewidth=2)

plt.title('CMB Spectrum')
plt.xlabel('Photon Energy [eV]')
plt.ylabel(r'$dn_\gamma/dE$ [eV$^{-1}$ cm$^{-3}$]')
plt.axis([1e-8, 100, 1e5, 1e17])
```

## `Spectra` - Sums, Integration over each Spectrum

Integration over each spectrum can be done using [*Spectra.integrate_each_spec()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.integrate_each_spec), with the result being an array. There are many reasons to perform an integration over each spectrum; here, we will show only a trivial example, integrating over the spectrum to get the total number. To do this, we use [*Spectra.integrate_each_spec()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.integrate_each_spec) with a weight given by $E \, d \log E$. Like [*Spectrum*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectrum/darkhistory.spec.spectrum.Spectrum.html), [*Spectra*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html) also has [*Spectra.totN()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.totN) and [*Spectra.toteng()*](https://darkhistory.readthedocs.io/en/latest/_autosummary/darkhistory/spec/spectra/darkhistory.spec.spectra.Spectra.html?highlight=spectra#darkhistory.spec.spectra.Spectra.toteng) functions, which return an array of numbers instead, one for each spectrum.

```python
log_bin_width = spectools.get_log_bin_width(CMB_specs.eng)
total_1 = CMB_specs.integrate_each_spec(CMB_specs.eng * log_bin_width)
total_2 = CMB_specs.totN()
print('Are the two results equal (up to floating point accuracy)?')
print(np.all(np.isclose(total_1, total_2, rtol=1e-15)))
```


# Selección óptima de portafolios I

En la clase pasada vimos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC (depende de las preferencias particulares - aversión al riesgo).

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En el siguiente análisis:

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos?
 - ¿Cuál es el mejor portafolio de activos riesgosos?
 - Es un portafolio eficiente en media-varianza.
- Problema: dado un conjunto de activos riesgosos, ¿cómo construimos la mejor combinación?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos dos activos riesgosos?

Cuando tenemos dos o más activos riesgosos, tenemos disponibles diferentes LAC. ¿Qué significan sus pendientes?

 Ver en el tablero.

Pregunta:
- ¿Qué es lo que se quiere?

**Conclusión:**
- El mejor portafolio de activos no depende de las preferencias individuales, y por tanto va a ser igual para todos.
- Dicho mejor portafolio maximiza el radio de Sharpe.
- A este portafolio lo llamaremos el portafolio eficiente en media-varianza (EMV)

**Idea principal: el portafolio óptimo de activos riesgosos es independiente de las preferencias del inversionista.**
- El portafolio EMV determina el portafolio óptimo de activos riesgosos.
- Todos tendremos el mismo portafolio de activos riesgosos (EMV), y lo combinaremos con el activo libre de reisgo de acuerdo con las preferencias de cada uno de nosotros (aversión al riesgo).
- La LAC combinando el activo libre de riesgo y el portafolio EMV, se vuelve el conjunto de portafolios eficientes.

Entonces, se deben seguir los siguientes pasos:
1. Crear la frontera media-varianza.
2. Encontrar el portafolio que maximize el radio de Sharpe (portafolio EMV).
3. Construir la frontera eficiente (LAC) del punto $(0,r_f)$ al punto $(\sigma_s,E[r_s])$ del portafolio EMV.
4. Combinar de acuerdo a sus preferencias.
___

## 2. Solución analítica del portafolio EMV: caso con dos activos.

Queremos solucionar el siguiente problema:

\begin{align}
\max_{w_1,w_2} &\quad \frac{E[r_p]-r_f}{\sigma_p}\\
\text{s.a.}    &\quad E[r_p]=w_1E[r_1]+w_2E[r_2]\\
               &\quad \sigma_p=\sqrt{w_1^2\sigma_1^2+w_2^2\sigma_2^2+2w_1w_2\rho_{12}\sigma_1\sigma_2}\\
               &\quad w_1+w_2=1, \quad w_1,w_2\geq0
\end{align}

el cual es equivalente a

\begin{align}
\max_{w_1} &\quad \frac{w_1E[r_1]+(1-w_1)E[r_2]-r_f}{\sqrt{w_1^2\sigma_1^2+(1-w_1)^2\sigma_2^2+2w_1(1-w_1)\rho_{12}\sigma_1\sigma_2}}\\
\text{s.a.}    &\quad 0\leq w_1\leq1
\end{align}

**Actividad.**
El anterior es un problema de maximizar una función de una variable en un dominio cerrado. No debaría representar dificultad.

Encontrar la solución analítica a este problema.

Quien primero lo haga, y salga a explicarlo al tablero, le subo alguna tarea o quiz a 100.

Deben llegar a:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

Si nadie lo ha hecho en 30 min., procederé a hacerlo yo.

**Nota:**
- así como obtuvimos una expresión para el peso del portafolio de mínima varianza con dos activos, obtenemos una expresión para el peso del portafolio Eficiente en Media-Varianza.
- Estas actividades son sin duda un buen ejercicio, y se pueden replicar usando técnicas de varias variables (multiplicadores de Lagrange) cuando se tengan más de dos activos.
- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros.
- Por eso, les enseño cómo resolver este problema con optimizadores numéricos, porque son una solución viable y escalable a más variables.

## 3. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

Supondremos, además, que la tasa libre de riesgo es $r_f=5\%$.

```python
# Tasa libre de riesgo
rf = 0.05
```

Entonces, supondremos que tenemos disponibles los activos correspondientes a los mercados de acciones de EU y Japón, y en adición el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

```python
# Vector de w variando entre 0 y 1 con n pasos
N = 101
w = np.linspace(0, 1, N)
# Rendimientos esperados individuales
# Activo1: EU, Activo2:Japon
E1 = annual_ret_summ.loc['Media', 'EU']
E2 = annual_ret_summ.loc['Media', 'Japon']
# Volatilidades individuales
s1 = annual_ret_summ.loc['Volatilidad', 'EU']
s2 = annual_ret_summ.loc['Volatilidad', 'Japon']
# Correlacion
r12 = corr.loc['EU', 'Japon']
# Covarianza
s12 = s1 * s2 * r12
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
portafolios = pd.DataFrame({'w': w,
                            '1-w': 1 - w,
                            'Media': w * E1 + (1 - w) * E2,
                            'Vol': ((w * s1)**2 + ((1 - w) * s2)**2 + 2 * w * (1 - w) * s12) ** 0.5
                           })
portafolios['RS'] = (portafolios['Media'] - rf) / portafolios['Vol']
portafolios.head()
```

      w
      1-w
      Media
      Vol
      RS

      0
      0.00
      1.00
      0.149700
      0.229800
      0.433856

      1
      0.01
      0.99
      0.149558
      0.227916
      0.436820

      2
      0.02
      0.98
      0.149416
      0.226041
      0.439814

      3
      0.03
      0.97
      0.149274
      0.224176
      0.442839

      4
      0.04
      0.96
      0.149132
      0.222322
      0.445894

```python
# Importar librerías de gráficos
from matplotlib import pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a RS
plt.figure(figsize=(6, 4))
plt.plot(s1, E1, 'ro', ms=10, label='EU')
plt.plot(s2, E2, 'bo', ms=10, label='Japón')
plt.scatter(portafolios['Vol'], portafolios['Media'], c=portafolios['RS'], cmap='inferno', label='Portafolios')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
```

#### 2. Encontrar el portafolio que maximiza el radio de Sharpe (EMV)

Primero, encontramos este portafolio con la fórmula que obtuvimos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

```python
# Fórmula que obtuvimos
w_EMV = ((E1 - rf) * s2**2 - (E2 - rf) * s12) / ((E2 - rf) * s1**2 + (E1 - rf) * s2**2 - (E1 - rf + E2 - rf) * s12)
w_EMV
```

    0.6983139170512034

Ahora sí, con la función scipy.optimize.minimize

```python
# Importar la función minimize del módulo optimize
from scipy.optimize import minimize
```

```python
# Función objetivo (-RS)
def menos_RS(w, E1, E2, s1, s2, s12, rf):
    Erp = w * E1 + (1 - w) * E2
    sp = ((w * s1)**2 + ((1 - w) * s2)**2 + 2 * w *(1 - w) * s12)**0.5
    RS = (Erp - rf) / sp
    return -RS
```

```python
# Dato inicial
w0 = 0.5
# Cotas de las variables
bnd = ((0, 1),)
```

```python
# Optimización numérica
res = minimize(fun=menos_RS,
               x0=w0,
               args=(E1, E2, s1, s2, s12, rf),
               bounds=bnd
              )
# Resultado
res
```

          fun: array([-0.63087253])
     hess_inv: <1x1 LbfgsInvHessProduct with dtype=float64>
          jac: array([3.39728246e-06])
      message: b'CONVERGENCE: NORM_OF_PROJECTED_GRADIENT_<=_PGTOL'
         nfev: 10
          nit: 4
       status: 0
      success: True
            x: array([0.69831582])

```python
w_EMV, res.x
```

    (0.6983139170512034, array([0.69831582]))

Con lo anterior, podemos obtener datos de rendimiento esperado y volatilidad del portafolio EMV

```python
# Rendimiento esperado y volatilidad del portafolio EMV
E_EMV = w_EMV * E1 + (1 - w_EMV) * E2
s_EMV = ((w_EMV * s1)**2 + ((1 - w_EMV) * s2)**2 + 2 * w_EMV * (1 - w_EMV) * s12)**0.5
E_EMV, s_EMV
```

    (0.13978394237787292, 0.14231708951606933)

```python

```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, y portafolio EMV
plt.figure(figsize=(6, 4))
plt.plot(s1, E1, 'ro', ms=10, label='EU')
plt.plot(s2, E2, 'bo', ms=10, label='Japón')
plt.scatter(portafolios['Vol'], portafolios['Media'], c=portafolios['RS'], cmap='inferno', label='Portafolios')
plt.plot(s_EMV, E_EMV, 'g*', ms=10, label='Port. EMV')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
```

#### 3. Construir LAC

Ahora, dibujamos la LAC, combinando el portafolio EMV con el activo libre de riesgo:

```python
# Vector de wp variando entre 0 y 1.5 con n pasos
wp = np.linspace(0, 1.5)
```

```python
# DataFrame de CAL:
# 1. Índice: i
# 2. Columnas 1-2: wp, wrf
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
LAC = pd.DataFrame({'w(EMV)': wp,
                    'w(rf)': 1 - wp,
                    'Media': wp * E_EMV + (1 - wp) * rf,
                    'Vol': wp * s_EMV
                   })
LAC['RS'] = (LAC['Media'] - rf) / LAC['Vol']
LAC.head()
```

      w(EMV)
      w(rf)
      Media
      Vol
      RS

      0
      0.000000
      1.000000
      0.050000
      0.000000
      NaN

      1
      0.030612
      0.969388
      0.052748
      0.004357
      0.630873

      2
      0.061224
      0.938776
      0.055497
      0.008713
      0.630873

      3
      0.091837
      0.908163
      0.058245
      0.013070
      0.630873

      4
      0.122449
      0.877551
      0.060994
      0.017427
      0.630873

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, portafolio EMV y LAC
plt.figure(figsize=(6, 4))
plt.plot(s1, E1, 'ro', ms=10, label='EU')
plt.plot(s2, E2, 'bo', ms=10, label='Japón')
plt.scatter(portafolios['Vol'], portafolios['Media'], c=portafolios['RS'], cmap='inferno', label='Portafolios')
plt.plot(s_EMV, E_EMV, 'g*', ms=10, label='Port. EMV')
plt.plot(LAC['Vol'], LAC['Media'], '--k', lw=2, label='LAC')
plt.xlabel('Volatilidad $\sigma$')
plt.ylabel('Rendimiento esperado $E[r]$')
plt.grid()
plt.legend(loc='best')
plt.colorbar()
plt.axis([0.13, 0.16, 0.13, 0.15])
```

#### 4. Combinación óptima de acuerdo a preferencias

Con los datos anteriores, y la caracterización de aversión al riesgo, se escoge la combinación óptima entre el portafolio EMV y el activo libre de riesgo de acuerdo a:

$$w^\ast=\frac{E[r_s-r_f]}{\gamma\sigma_s^2}.$$

```python
# Para gamma=7
g = 7
w_cap = (E_EMV - rf) / (g * s_EMV**2)
w_cap
```

    0.6332665142475

```python
w_cap * w_EMV, w_cap * (1 - w_EMV), 1 - w_cap
```

    (0.44221882010153346, 0.1910476941459666, 0.3667334857525)

Created with Jupyter by Esteban Jiménez Rodríguez.


```python
%matplotlib inline

import matplotlib
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (12, 9)
plt.rcParams["font.size"] = 18
```

# Reactor Physics

This is not a reactor physics class. However, to understand and analyze fuel management, advanced reactor designs, and spent fuel recycling, one must first have a background in reactor physics.

## Learning Objectives

- Describe criticality in terms of the life cycle of a fission neutron.
- Differentiate concepts such as neutron flux, reaction rates, cross sections, reactivity, multiplication factor.
- Calculate the multiplication factor using the 6 factor formula.
- Calculate the dynamic impact of reactivity feedback on criticality.
- Recognize the impact of moderator material choice on reactor behavior.
- Analyze reactor behavior.
- Calculate the impact of geometry on criticality.

## Neutron Multiplication Fundamentals

### Neutron interactions with matter.

\begin{align}
^1_0n + {^a_z}X \longrightarrow \left({^{a+1}_z}X\right)^*  \longrightarrow
\begin{cases}
^1_0n + {^a_z}X , & \mbox{Resonance Elastic Scattering}\\
^1_0n + \left({^a_z}X\right)^* , & \mbox{Elastic Scattering}\\
{^{a+1}_z}X +\gamma , & \mbox{Radiative Capture}\\
{^{a_1}_{z_2}}X + {^{a_2}_{z_2}}X + \nu^1_0n, & \mbox{Fission}
\end{cases}
\end{align}

### Cross sections

The likelihood of each of these events is captured by cross sections.

- $\sigma_x = $ microscopic cross section $[cm^2]$
- $\Sigma_x = $ macroscopic cross section $[1/length]$
- $\Sigma_x = N\sigma_x $
- $N = $ number density of target atoms $[\#/volume]$

### Cross sections are in units of area. Explain this to your neighbor.

### Reaction Rates

- The microscopic cross section is just the likelihood of the event per unit area.
- The macroscopic cross section is just the likelihood of the event per unit area of a certain density of target isotopes.
- The reaction rate is the macroscopic cross section times the flux of incident neutrons.

\begin{align}
R_{i,j}(\vec{r}) &= N_j(\vec{r})\int dE \phi(\vec{r},E)\sigma_{i,j}(E)\\
R_{i,j}(\vec{r}) &= \mbox{reactions of type i involving isotope j } [reactions/cm^2s]\\
N_j(\vec{r}) &= \mbox{number of nuclei participating in the reactions }\\
E &= \mbox{energy}\\
\phi(\vec{r},E)&= \mbox{flux of neutrons with energy E at position i}\\
\sigma_{i,j}(E)&= \mbox{cross section}\\
\end{align}

This can be written more simply as $R_x = \Sigma_x I N$, where I is intensity of the neutron flux.

### Neutron attenuation

$$
\begin{align}
I(x) &= I_0e^{-\Sigma_t x}\\
\end{align}
$$

where

$$
\begin{align}
        I(x) &= \mbox{intensity at distance x}\\
        I_0 &= \mbox{initial intensity}\\
        \Sigma_t &= \mbox{macroscopic total cross section} \\
        x &= \mbox{distance into material [m]}\\
\end{align}
$$

```python
import math
def attenuation(distance, initial=100, sig_t=1):
    """This function describes neutron attenuation into the slab"""
    return initial*math.exp(-sig_t*distance)

```

Rather than intensity, one can find the probability density:

In the case of decay:

\begin{align}
P(t)dt &= \lambda e^{-\lambda t}dt
\end{align}

From this, one can find the mean lifetime of a neutron before absorption:

\begin{align}
\bar{t} &= \int_0^\infty t'P(t')dt'\\
        &= \int_0^\infty t'\lambda e^{-\lambda t'}dt'\\
        &= \frac{1}{\lambda}
\end{align}

In the case of attenuation:
\begin{align}
P(x)dx &= \Sigma_te^{-\Sigma_tx}dx
\end{align}

So, the mean free path is:

\begin{align}
\bar{l} &= \int_0^\infty x'P(x')dx'\\
        &= \int_0^\infty x'\Sigma_te^{-\Sigma_t x'}dx'\\
        &= \frac{1}{\Sigma_t}
\end{align}

```python
def prob_dens(distance, initial=100, sig_t=1):
    return sig_t*attenuation(distance, initial=100, sig_t=1)

```

```python
sig_t = 0.2
i_0 = 100

# This code plots attenuation
import numpy as np
z = np.arange(24)
y = np.arange(24)
x = np.arange(24)
for h in range(0,24):
    x[h] = h
    y[h] = attenuation(h, initial=i_0, sig_t=sig_t)
    z[h] = prob_dens(h, initial=i_0, sig_t=sig_t)

# creates a figure and axes with matplotlib
fig, ax = plt.subplots()
scatter = plt.scatter(x, y, color='blue', s=y*20, alpha=0.4)
ax.plot(x, y, color='red')
ax.plot(x, z, color='green')

# adds labels to the plot
ax.set_ylabel('Percent of Neutrons')
ax.set_xlabel('Distance into slab')
ax.set_title('Attenuation')

# adds tooltips
import mpld3
labels = ['{0}% intensity'.format(i) for i in y]
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.display()
```

    /home/huff/anaconda3/lib/python3.5/site-packages/IPython/core/formatters.py:92: DeprecationWarning: DisplayFormatter._ipython_display_formatter_default is deprecated: use @default decorator instead.
      def _ipython_display_formatter_default(self):
    /home/huff/anaconda3/lib/python3.5/site-packages/IPython/core/formatters.py:669: DeprecationWarning: PlainTextFormatter._singleton_printers_default is deprecated: use @default decorator instead.
      def _singleton_printers_default(self):

### Source term

The source of neutrons in a reactor are the neutrons from fission.

\begin{align}
s &=\nu \Sigma_f \phi
\end{align}

where

\begin{align}
s &= \mbox{neutrons available for next generation of fissions}\\
\nu &= \mbox{the number born per fission}\\
\Sigma_f &= \mbox{the number of fissions in the material}\\
\phi &= \mbox{initial neutron flux}
\end{align}

This can also be written as:

\begin{align}
s &= \nu\Sigma_f\phi\\
  &= \nu\frac{\Sigma_f}{\Sigma_{a,fuel}}\frac{\Sigma_{a,fuel}}{\Sigma_a}\\
  &= \eta f \Sigma_a\phi\\
\eta &= \frac{\nu\Sigma_f}{\Sigma_{a,fuel}} = \mbox{number of neutrons produced per neutron absorbed by the fuel, "neutron reproduction factor"}\\
f &= \frac{\nu\Sigma_{a,fuel}}{\Sigma_a} = \mbox{number of neutrons absorbed in the fuel per neutron absorbed anywhere, "fuel utilization factor"}\\
\end{align}

This absorption and flux term at the end seeks to capture the fact that some of the neutrons escape. However, if we assume an infinite reactor, we know that all the neutrons are eventually absorbed in either the fuel or the coolant, so we can normalize by $\Sigma_a\phi$ and therefore:

\begin{align}
k_\infty &= \frac{\eta f \Sigma_a\phi}{\Sigma_a \phi}\\
&= \eta f
\end{align}

### Fission Spectrum

$\chi(E)$ is an empirical probability density function describing the energies of prompt fission neutrons.

\begin{align}
\chi (E) &= 0.453e^{-1.036E}\sinh\left(\sqrt{2.29E}\right)\\
\end{align}

```python
import numpy as np
import math
def chi(energy):
    return 0.453*np.exp(-1.036*energy)*np.sinh(np.sqrt(2.29*energy))

energies = np.arange(0.0,10.0, 0.1)

plt.plot(energies, chi(energies))
plt.title(r'Prompt Neutron Energy Distribution $\chi(E)$')
plt.xlabel("Prompt Neutron Energy [MeV]")
plt.ylabel("probability")
```

#### Questions about this plot:

- What is the most likely prompt neutron energy?
- Can you write an equation for the average neutron energy?

```python
### Max function
print(max([chi(e) for e in energies]), chi(0.7))
```

    0.358102702287 0.358102702287

#### Expectation Value

Recall that the average energy will be the expectation value of the probability density function.

\begin{align}
<\chi(E)> &= \int\chi(E)dE\\
&= E \chi(E)
\end{align}

```python
plt.plot(energies, [chi(e)*e for e in energies])
```

### What energy neutron do we prefer for fission in U235?

### Four Factor Formula

So, then, if we consider the impact of neutron energy on the likelihood of absorption, we can break the neutrons up into a thermal and a fast group.

So, we can say more explicitly:

\begin{align}
k_\infty =& \epsilon p \eta f\\
\epsilon =& \mbox{ fast fission factor}\\
         =& \frac{\mbox{neutrons produced by all fissions}}{\mbox{neutrons produced by thermal fissions}}\\
p =& \mbox{ resonance escape probability}\\
         =& \frac{\mbox{neutrons that reach thermal energies}}{\mbox{number of fast neutrons that start to slow down}}
\end{align}

If we consider the non-infinite nature of most reactors, we have to add two more factors to the formula:

\begin{align}
k_\infty =& P_{fnl} P_{tnl}\epsilon p \eta f\\
P_{fnl} =& \mbox{ fast neutron leakage probability}\\
         =& \frac{\mbox{fast neutrons that do not leak from the reacotr}}{\mbox{fast neutrons produced by thermal fissions}}\\
P_{tnl} =& \mbox{ thermal neutron leakage probability}\\
         =& \frac{\mbox{thermal neutrons that do not leak from the reacotr}}{\mbox{neutrons that reach thermal energies}}\\
\end{align}

## Neutron Transport Fundamentals

### Fast Homogenous Reactor

Consider a reactor that is:

- fast
- critical
- the fuel and coolant are a homogeneous mixture
- the reactor has only one region, no reflector ("bare" reactor)

This reactor can be described by the one group diffusion equation:

\begin{align}
D\nabla^2\phi-\Sigma_a\phi + s &= \frac{1}{v}\frac{\partial \phi}{\partial t}\\
D &= \mbox{ the diffusion coefficient}\\
\phi &= \mbox{ flux}\\
v &= \mbox{ neutron speed}
\end{align}

If the fission source, $s$ does not balance neutron absorption and leakage, then the right hand side of the one-group diffusion equation is nonzero and the power may increase or decrease with time.

### Reactivity is deviation from k

\begin{align}
\rho &= \frac{k-1}{k}
\end{align}

```python
def rho(k):
    """reactivity, rho
    :param k: multiplication factor
    """
    return (k-1)/k

# Plot reactivity as a function of k
import numpy as np
k_vals = np.arange(0.7, 2.0, 0.01)
r = [rho(k) for k in k_vals]
plt.plot(k_vals, r)
plt.ylabel(r'reactivity $\rho$ $[\frac{\delta k}{k}]$',fontsize=20,)
plt.xlabel(r'multiplication factor $k$ $[-]$',fontsize=20,)
plt.title(r'$\rho = \frac{k-1}{k}$', fontsize=20, y=1.02)
```

#### Steady state

The multiplication factor, k, can be used to adjust the source strength and reach a steady state diffusion equation:

\begin{align}
D\nabla^2\phi-\Sigma_a\phi + \frac{1}{k}\nu\Sigma_f\phi &= 0\\
\end{align}

## Diffusion Solution

```python
## This code example was adapted from
## https://github.com/marort91/AlphaEigenvalue/blob/master/RadiationTransportCoding/NeutronDiffusion_Python/NDE_CriticalityEigenvalue.ipynb

def diff(sig_tr):
    return 1.0/(3.0*sig_tr)

def sig_tr(e):
    sig_t(e) - mu*sig_s(e)

D = diff(0.1)
nusigf = 0.70
siga = 0.066

Lx = np.pi*((nusigf-siga)/D)**(-0.5)

N = 50;
h = Lx/(N-1)

x = np.zeros(N)

for i in range(N-1):
    x[i+1] = x[i] + h

L = np.zeros((N,N))
A = np.zeros((N,N))
M = np.zeros((N,N))

for i in range(N):
    L[i][i] = L[i][i] + (-2*(-D/(h**2)))

for i in range(1,N):
    L[i][i-1] = L[i][i-1] + (-D/h**2)

for i in range(N-1):
    L[i][i+1] = L[i][i+1] + (-D/h**2)

for i in range(N):
    A[i][i] = A[i][i] + siga

M = L + A

M[0][0] = 1
M[0][1] = 0
M[N-1][N-1] = 1
M[N-1][N-2] = 0

phi0 = np.ones((N,1))
phi0[0] = 0
phi0[N-1] = 0

tol = 1e-15
k = 1.00

def is_converged(k_old, k, tol):
    return np.abs(k - k_old) <= tol

for i in range(100):
    # update k
    k_old = k
    # solve for psi
    psi = np.linalg.solve(M, nusigf*phi0)

    # solve for k
    k = sum(nusigf*psi)/sum(nusigf*phi0)

    # solve for phi
    phi0 = (1/k)*psi
    phi0[0] = 0
    phi0[N-1] = 0

    # determine convergence
    if is_converged(k_old, k, tol):
        break

plt.plot(x, phi0)
plt.xlabel('Slab (cm)')
plt.ylabel('Neutron Flux')
plt.grid()

print("k-effective = ", k)

print(" approx alpha = ", rho(k) * sum(nusigf*phi0)/sum(phi0))
```

### One Group Reactor Equation

We can define a quantity, geometric bucking, as:

\begin{align}
B^2 &= \frac{1}{D}\left(\frac{\nu}{k}\Sigma_f - \Sigma_a\right)\\
\end{align}

Next, we can simplify the previous equation using this definition, so that the one-group reactor equation becomes:

\begin{align}
\nabla^2\phi + B^2\phi &= 0\\
\end{align}

To find the criticality of a reactor with a specific geometry, then, we can solve the geometric buckling equation for k:

\begin{align}
k &= \frac{\nu\Sigma_f}{DB^2 + \Sigma_a}\\
\end{align}

The buckling, B, is used to help describe the geometry of the system. The solutions of the one group reactor equation for boundary conditions corresponding to canonical shapes provide both flux and buckling formulae for each canonical shape:

#### Slab
Thickness a:
\begin{align}
\phi &= cos\left(\frac{\pi x}{a}\right)\\
B^2 &= \frac{\pi^2}{a^2}
\end{align}

#### Cylinder
Height H, Radius R:
\begin{align}
\phi &= J_0\left(\frac{\nu_0r}{R}\right)cos\left(\frac{\pi z}{H}\right)\\
B^2 &= \frac{\nu_0^2}{R^2} + \frac{\pi^2}{H^2}
\end{align}

#### Sphere
Radius R:
\begin{align}
\phi &= \left(\frac{1}{r}\right)sin\left(\frac{\pi r}{R}\right)\\
B^2 &= \frac{\pi^2}{R^2}
\end{align}

```python
def k(nusigf, diff, bsq, siga):
    return nusigf/(diff*bsq + siga)

def bsq_sphere(r):
    return (np.pi**2)/(r**2)

nusigf = 0.3
diff = 1.1
siga =0.01

fig, ax1 = plt.subplots()
radii = np.arange(0.1, 5.0, 0.01)
ax1.plot(radii, k(nusigf, diff, bsq_sphere(radii), siga), 'b-', label='k')
ax1.set_xlabel('radius (r)')
ax1.set_ylabel('k', color='b')
ax1.set_title('Criticality and Geometric Buckling of a Sphere')
for tl in ax1.get_yticklabels():
    tl.set_color('b')

ax2 = ax1.twinx()

ax2.plot(radii,  bsq_sphere(radii), 'darkorange', label=r'$B^2$')
ax2.set_ylabel(r'$B^2$', color='darkorange')
for tl in ax2.get_yticklabels():
    tl.set_color('darkorange')

ax1.legend()
ax2.legend(loc=2)
```

### Multigroup and beyond

To capture the variation of neutron energies in the diffusion equation, we can discretize the flux into energy groups.

\begin{align}
\phi &= \sum_{g=1}^{g=G}\phi_g\\
\phi_g &= \int_{E_g}^{E_{g-1}}\phi(E)dE\rvert_{g = 1,G}
\end{align}

The diffusion coefficient also needs to be individually evaluated for each energy group such that we have a $D_g$ for each group g. Also, it is important to consider possible paths of demise for potential fission neutrons.

The derivation, based on integration, can be found in your book, pages 128-130. It is based on the following integration:

\begin{align}
\int_{E_g}^{E_{g-1}}dE\left[-\nabla D(\vec{r},E)\nabla\phi(\vec{r},E)+ \Sigma_t(\vec{r},E)\phi(\vec{r},E)\right] &= \int_{E_g}^{E_{g-1}}dE\left[\int_{E'}\Sigma_{s}(\vec{r},E'\rightarrow E)\phi(\vec{r},E') + S(\vec{r},E)\right]\\
\end{align}

Once this integration is completed for a set of group boundaries, $g\in[1,G] \rightarrow E_g\in[E_1,E_G]$, one can generically write:

\begin{align}
-\nabla D_g\nabla\phi_g+ \Sigma_{t,g}\phi_g &= \sum_{g'=1}^{g'=G}\Sigma_{s}^{g'\rightarrow g}\phi_{g'} + S\int_{E_g}^{E_{g-1}}\chi(E)dE\\
\end{align}

Recall, however, that the fission spectrum is a probability density function, so one can also write the following identities:

\begin{align}
\int_0^\infty\chi(E) &= 1\\
\chi_g &= \int_{E_g}^{E_{g-1}}\chi(E)dE\\
\sum_{g=1}^{g=G}\chi_g &= 1\\
\end{align}

This simplifies the diffusion equation:

\begin{align}
-\nabla D_g\nabla^2\phi_g+ \Sigma_{t,g}\phi_g &= \sum_{g'=1}^{g'=G}\Sigma_{s}^{g'\rightarrow g}\phi_{g'} + \chi_gS\\
\end{align}

where

\begin{align}
S &= \sum_{g'=1}^{g'=G}(\nu\Sigma_f)_{g'}\phi_{g'}
\end{align}

#### Group Removal Cross Section

The right hand side summation of the scattering cross section is confusing though. Most of the scattering is from other groups into this one, but some of the scattering is from this group into itself. Keeping that term, $\Sigma_s^{g\rightarrow g} on the right hand side is misleading. It's not a source of new neutrons. So, we have a change of variables that can clean this up, the group removal cross section.

\begin{align}
\Sigma_{R,g} &= \Sigma_{t,g} - \Sigma_{s}^{g\rightarrow g}
\end{align}

If we use the group removal cross section, then we arrivae at the following form of the multigroup equation.

\begin{align}
-\nabla D_g\nabla^2\phi_g+ \Sigma_{R,g}\phi_g &= \sum_{g'\ne G}^{g'=G}\Sigma_{s}^{g'\rightarrow g}\phi_{g'} + \chi_g\sum_{g'=1}^{g'=G}(\nu\Sigma_f)_{g'}\phi_{g'}\\
\end{align}

### Two Group Diffusion

Let's define just two groups, fast and thermal. Let's also state that all prompt neutrons are born fast and that the diffusion coefficient and cross sections do not vary in space. With these assumptions, we arrive at the following two equations:

\begin{align}
-D_1\nabla^2\phi_1 + \Sigma_{R,1}\phi_1 &= \Sigma_{s}^{2\rightarrow 1}\phi_{2} + \left[(\nu\Sigma_f)_{1}\phi_{1} + (\nu\Sigma_f)_{2}\phi_{2}\right]\\
\end{align}

\begin{align}
- D_2\nabla\phi_2+ \Sigma_{R,2}\phi_2 &= \Sigma_{s}^{1\rightarrow 2}\phi_{1}\\
\end{align}

**Discussion: What happened to the prompt fission spectrum, $\chi_g$ in the above equations?**

**Discussion: If we neglect upscattering, which term or terms in the above two equations will disappear?**

### Criticality calculation

For criticality calculations, one might normalize the prompt fission spectrum with k as we have done before. For simplicity, one can move the scattering term to the left hand side, as if to say "diffusion, group removal, and scattering are balanced at criticality."

\begin{align}
-\nabla D_g\nabla\phi_g+ \Sigma_{R,g}\phi_g - \sum_{g'\ne G}^{g'=G}\Sigma_{s}^{g'\rightarrow g}\phi_{g'} &= \frac{1}{k}\chi_g\sum_{g'=1}^{g'=G}(\nu\Sigma_f)_{g'}\phi_{g'}\\
\end{align}

This change propagates to the explicit equations for two groups thus:

\begin{align}
- D_1\nabla^2\phi_1 + \Sigma_{R,1}\phi_1 - \Sigma_{s}^{2\rightarrow 1}\phi_{2} &= \frac{1}{k}\chi_1\left[(\nu\Sigma_f)_{1}\phi_{1} + (\nu\Sigma_f)_{2}\phi_{2}\right]\\
\end{align}

\begin{align}
- D_2\nabla^2\phi_2+ \Sigma_{R,2}\phi_2 - \Sigma_{s}^{1\rightarrow 2}\phi_{1} &= 0\\
\end{align}

### Addition of chemical shim

Together let us consider the impact of a chemical shim, some absorber introduced to the coolant.

**Discussion: In the two group equations, which parameter will change?**

In the thermal group, group 2, the removal cross section will change, because it involves absorption. The amount of chemical shim will impact the absorption cross section thus:

\begin{align}
\Sigma_{a',2} &= \Sigma_{a,2} + \Sigma_{shim,2} \\
\Sigma_{shim, 2} &= \Sigma_{a',2} - \Sigma_{a,2}\\
&= \rho_{shim}\frac{N_{avo}}{A_{shim}}\sigma_{a,shim,2}\\
\end{align}

## Wrap-up

- Describe criticality in terms of the life cycle of a fission neutron.
- Differentiate concepts such as neutron flux, reaction rates, cross sections, reactivity, multiplication factor.
- Calculate the multiplication factor using the 6 factor formula.
- Calculate the dynamic impact of reactivity feedback on criticality.
- Recognize the impact of moderator material choice on reactor behavior.
- Analyze reactor behavior.
- Calculate the impact of geometry on criticality.

## References

This section was developed to complement pages YY-ZZ of [1].

[1] N. Tsoulfanidis, The Nuclear Fuel Cycle. La Grange Park, Illinois, USA: American Nuclear Society, 2013.

```python

```