# Solar System Cost Analysis

## Objective

How much an energy system cost is a very important standard to judge if this solar storage setup is good or not? In this example, I will discover the power of mathematical optimization in addressing a common energy industry problem: solar storage setup cost analysis.

The current ERCOT solar grid will be evaluated based on the historical Houston Hub settlement point price along with the normalized solar output in 2019.

---
## Problem Description

In this problem, a renewable energy developer want to make an investment to a new solar farm in Houston. However, they want to know what kind of configurations is most economical and beneficial for them. Therefore, an analysis on how much they would earn in 2019 by comparing three different configurations is implemented. Here, those three configurations are respectively (1) standalone storage system; (2) solar+storage system (3) standalone solar.

The data set of 8760 hourly solar irradiance and Houston hub electricity price is as follows:

| weather year hour beginning | Solar Shape (Normalized) | Houston hub Settlement Point Energy Price (\$/MWh) |
| --- | --- | ---|
| 1/1/2019 0:00| 0 | 14.1900	 |
| 1/1/2019 1:00 | 0 | 15.1825	 |
| 1/1/2019 2:00 | 0 | 15.5300 |

(1) Proposed Installation (Standalone Solar) Setting:

| Parameter | Value| Unit |
| --- | --- | --- |
| Solar Size | 300 |  MW |
| Inverter Capacity | 220 | MW |
| Solar System Cost | \$1.45 | \$/Watt-DC |

(2) Proposed Installation (Standalone Storage) Setting:

| Parameter | Value| Unit |
| --- | --- | --- |
| Storage Capacity | 50 |  MW |
| Storage Energy | 200 | MWh |
| Round Trip Efficiency | 85\% |   |
| Storage System Cost | \$350 | \$/KWh  |

(3) Proposed Installation (Combined Solar + Storage System) Setting:

| Parameter | Value| Unit |
| --- | --- | --- |
| Solar Size | 300 |  MW |
| Inverter Capacity | 220 | MW |
| Storage Capacity | 50 |  MW |
| Storage Energy | 200 | MWh  |
| Round Trip Efficiency | 85\% |  |
| Solar System Cost | \$1.35 | \$/Watt-DC  |
| Storage System Cost | \$325 | \$/KWh  |

---

## Model Formulation

### Parameters
$\text{RTM} \in \mathbb{R}^+$: Real Time Market Settlement Point Price  \$/KWh.

normSolar: Normalized Solar Shape.

timeStamp: hours begining in year 2019.

Life cycle of Solar Pannel: assume 30 years in service.

Life cycle of Battery Storage: assume 10 years in service.

$\eta$: Round Trip Efficiency

$SOC_t$: State of Charge at hour t

$AC_t$: AC output of solar energy at hour t

$DC_t$: DC output of solar energy at hour t

### Decision Variables

$SOC_t$: State of Charge at hour t

$E_t^{in}$: Energy delivered to the storage at hour t

$E_t^{out}$: Energy discharged to the grid at hour t

$E_t^{Gin}$ : Energy charged to the storage from grid at hour t

$E_t^{BoutG}$ : Energy discharged to the grid from storage at hour t

$E_t^{Sin}$ : Energy delivered to the storage from solar at hour t

$E_t^{SoutG}$   : Energy delivered to the grid from solar at hour t

### Objective Function

- **𝑅𝑒𝑣𝑒𝑛𝑢𝑒**: Maximize the revenue of solar system (in USD).

For a standalone solar system (Assume 30 years solar plant life):

\begin{equation}
\text{Maximize}  AC_t \times P_t
\end{equation}

For a standalone storage system (Assume 10 years storage life):

\begin{equation}
\text{Maximize}  (E_t^{out}- E_t^{in}) \times P_t
\end{equation}

For a solar and storage system (Assume 10 years storage life, 30 years solar plant life):

\begin{equation}
\text{Maximize} (E_t^{BoutG}+E_t^{SoutG})∗P_t− E_t^{Gin}∗P_t
\end{equation}

### Constraints

For a standalone solar system:

$DC_t$ = 300*normSolar

$AC_t$ = min(220, $DC_t$)

For a standalone storage system:
(1) at any time t, the SOC will follow
\begin{equation}
SOC_t=SOC_{t-1}+\sqrt{\eta}\times E_{t-1}^{in}-E_{t-1}^{out}
\end{equation}

(2) $\forall t$, $0<=SOC_t<=200$.

(3) $\forall t$, $0<=\sqrt{\eta}\times E_{t}^{in}<=50$.

(4) $\forall t$, $0<=E_{t}^{out}<=50$.

(5) $\forall t$, $0<=E_{t}^{out}<=SOC_t$.

For a system combined solar and battery storage:

(1) at any time t, the SOC will follow
\begin{equation}
SOC_t=SOC_{t-1}+ (E_{t-1}^{Gin}+E_{t-1}^{Sin})-E_{t-1}^{BoutG}
\end{equation}

(2) $\forall t$, $0<=SOC_t<=200$.

(3) $\forall t$, $0<=(E_{t}^{Gin}+E_{t}^{Sin}) <=50$.

(4)  $\forall t$, $0<=E_{t}^{BoutG}<=50$.

(5)  $\forall t$, $0<=E_{t}^{BoutG}<=SOC_t$.

---
## Python Implementation

We import the Pyomo and Gurobi Python Module and other Python libraries such as pandas, NumPy.

```python
import pandas as pd
import numpy as np
import dataframe_image as dfi
import datetime
from pyomo.environ import *
import warnings
import matplotlib.pyplot as plt
warnings.filterwarnings('ignore')
```

## Input Data
We define all the input data of the model.

```python
file_loc = "./case/case/price/pricee.xlsx"

dti = pd.date_range("2019-01-01", periods=8760, freq="H")

sheets_dict = pd.read_excel(file_loc, sheet_name=None)

dflist=[]
for i in sheets_dict.values():
    dfnewprice = i.loc[i['Settlement Point Name'] == 'HB_HOUSTON']
    df3 = dfnewprice['Settlement Point Price']
    df3.index = range(len(df3.index))
    df6 = df3.groupby(df3.index // 4).mean()
    dflist.append(df6)

dfhourprice = pd.concat(dflist)
dfhourprice.index = dti
dfhourprice = dfhourprice.to_frame()

# solar shape
file_loc = "./case/case/FuelMixReport_PreviousYears/IntGenbyFuel2019.xlsx"
sheetname= ['Jan', 'Feb', 'Mar', 'Apr', 'May','Jun', 'Jul', 'Aug','Sep', 'Oct', 'Nov', 'Dec']

dfpricelist=[]

for i in sheetname:
    df_price_map = pd.read_excel(file_loc, sheet_name=i)
    dfnewprice = df_price_map.loc[df_price_map['Fuel'] == 'Solar']
    dfnewprice1 = dfnewprice.iloc[: , 4:]
    dfnewprice1 = dfnewprice1.groupby(np.arange(len(dfnewprice1.columns))//4, axis=1).mean()
    dfnewprice1.index = range(len(dfnewprice1.index))
    dfnewprice1_transposed = dfnewprice1.T
    dfnewprice1_transposed1 = pd.Series(dfnewprice1_transposed.values.ravel('F'))
    dfpricelist.append(dfnewprice1_transposed1)

dfnormsolar = pd.concat(dfpricelist)

normalized_dfnormsolar=dfnormsolar/dfnormsolar.max()
normalized_dfnormsolar.index = dti

df = dfhourprice.merge(normalized_dfnormsolar.rename('SolarShape'), left_index=True, right_index=True)
```

```python
# Monthly solar generation in MWh at 2019 in ERCOT region
monthly_solar = df.groupby(pd.PeriodIndex(df.index, freq="M"))['SolarShape'].sum()
dfsolarMonthly = monthly_solar*dfnormsolar.max()*4/1000 # in GWh

fig, ax = plt.subplots()
dfsolarMonthly.plot.bar(rot=70)
fig.tight_layout()
fig.savefig('monthly_solar.png')
```

## Compute the monthly averaged historical  Settlement Point Price for the year of 2019

```python
monthly_averages = df.groupby(pd.PeriodIndex(df.index, freq="M"))['Settlement Point Price'].mean()
hourly_averages = df.groupby([df.index.month, df.index.hour])['Settlement Point Price'].mean()
dfavg1 = pd.DataFrame()

for i in range (1,13):
    for j in range (24):
        dftemp=df.loc[(df.index.month==i) & (df.index.hour==j), 'Settlement Point Price']
        dfavg1.loc[i-1,j+1] = round(dftemp.mean())
dfavg1.index = ['01', '02', '03', '04','05','06','07','08','09','10','11','12']
dfavg1.loc['Avg'] = round(dfavg1.mean(axis=0))

dfavg1['Avg'] = round(dfavg1.mean(axis=1))
dfavg1 = dfavg1.astype(int)
df_styled = dfavg1.style.background_gradient(axis=None, vmin=10, vmax=50, cmap='coolwarm')
dfi.export(df_styled,"Monthly_Average_Price.png")
dfavg1.to_excel("avgRTM.xlsx")
df_styled
```

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      Avg

      01
      18
      17
      17
      18
      19
      22
      37
      29
      30
      30
      26
      26
      22
      21
      21
      20
      21
      32
      31
      26
      26
      24
      22
      19
      24

      02
      18
      16
      16
      17
      17
      19
      34
      26
      27
      35
      33
      23
      22
      22
      21
      20
      20
      22
      31
      24
      25
      24
      20
      19
      23

      03
      16
      15
      15
      16
      19
      33
      59
      29
      29
      30
      28
      28
      26
      28
      25
      24
      25
      26
      53
      30
      31
      25
      20
      18
      27

      04
      14
      14
      13
      14
      17
      43
      25
      20
      25
      23
      24
      23
      27
      39
      37
      42
      34
      32
      31
      43
      24
      21
      18
      17
      26

      05
      16
      15
      15
      16
      17
      22
      20
      22
      21
      25
      27
      31
      42
      51
      48
      39
      29
      26
      27
      25
      24
      21
      18
      18
      26

      06
      16
      15
      14
      15
      16
      16
      17
      18
      20
      22
      25
      30
      36
      41
      69
      90
      48
      26
      27
      23
      21
      19
      18
      17
      27

      07
      16
      15
      14
      14
      15
      16
      17
      18
      19
      22
      25
      45
      51
      63
      88
      91
      47
      27
      25
      25
      22
      20
      18
      18
      30

      08
      15
      14
      13
      13
      14
      15
      16
      18
      19
      22
      30
      38
      164
      599
      928
      828
      258
      44
      26
      25
      21
      19
      17
      16
      132

      09
      16
      15
      15
      15
      16
      18
      18
      20
      24
      26
      28
      43
      77
      121
      199
      299
      99
      32
      34
      27
      22
      21
      18
      17
      51

      10
      15
      14
      13
      13
      15
      33
      26
      19
      20
      21
      22
      24
      27
      54
      124
      96
      42
      25
      25
      21
      22
      18
      19
      16
      30

      11
      23
      17
      17
      19
      19
      21
      33
      25
      30
      31
      30
      24
      21
      20
      21
      21
      24
      50
      23
      29
      24
      22
      26
      20
      25

      12
      14
      14
      13
      14
      15
      16
      25
      18
      17
      19
      17
      17
      16
      16
      15
      15
      16
      22
      14
      18
      17
      16
      16
      15
      16

      Avg
      16
      15
      15
      15
      17
      23
      27
      22
      23
      26
      26
      29
      44
      90
      133
      132
      55
      30
      29
      26
      23
      21
      19
      18
      36

```python
sp1={'Solar Size': 300.0, 'Inverter Capacity': 220.0, 'Solar System Cost': 1.45}

sp2={'Storage Capacity': 50,
 'Storage Energy': 200,
 'Round Trip Efficiency': 0.85,
 'Storage System Cost': 350}

sp3={'Solar Size': 300.0,
 'Inverter Capacity': 220.0,
 'Storage Capacity': 50.0,
 'Storage Energy': 200.0,
 'Round Trip Efficiency': 0.85,
 'Solar System Cost': 1.35,
 'Storage System Cost': 325}
```

### case 1: Calculate the revenue of Standalone Solar configuration

```python
df['SolarDC'] = df['SolarShape'].values * sp1['Solar Size']
df['SolarAC'] = df['SolarDC'].apply(lambda x: x if x <= sp1['Inverter Capacity'] else sp1['Inverter Capacity'])
df['hour'] = range(0,8760)
df.index = df.hour
results_df = df
results_df['revenue'] = df.SolarAC * df['Settlement Point Price']
serviceLifeSolar = 30

total_revenue = results_df.revenue.sum()
capex = sp1['Solar Size']*sp1['Solar System Cost']*1000000/serviceLifeSolar

print('Annual revenue was ${:,.2f}'.format(total_revenue))
print('Annual cost ${:,.2f}'.format(capex))
```

    Annual revenue was $42,620,516.89
    Annual cost $14,500,000.00

### Case 2: Calculate the revenue of Standalone Storage configuration

```python
def optimizeStorage(df, first_model_hour=0, last_model_hour=8759, setup=sp2):
    df = df.loc[first_model_hour:last_model_hour, :]

    model = ConcreteModel()

    # Define model parameters
    model.T = Set(doc='hour of year', initialize=df.hour.tolist(), ordered=True)
    model.Rmax = Param(initialize=setup['Storage Capacity'],
                       doc='Max rate of power flow (MW) in or out')
    model.Smax = Param(initialize=setup['Storage Energy'], doc='Max storage (MWh)')
    model.P = Param(initialize=df['Settlement Point Price'].tolist(), doc='RTMP for each hour')

    # Charge, discharge, and state of charge
    model.Ein = Var(model.T, domain=NonNegativeReals)
    model.Eout = Var(model.T, domain=NonNegativeReals)
    model.S = Var(model.T, bounds=(0, model.Smax))

    #Set all constraints
    def storage_state(model, t):
        'Storage changes with flows in/out and efficiency losses'
        if t == model.T.first():
            return model.S[t] == 0

        else:
            return (model.S[t] == (model.S[t-1]
                                + (model.Ein[t-1] * np.sqrt(eta))
                                - (model.Eout[t-1])))

    model.charge_state = Constraint(model.T, rule=storage_state)

    def discharge_constraint(model, t):
        "Maximum dischage within a single hour"
        return model.Eout[t] <= model.Rmax

    model.discharge = Constraint(model.T, rule=discharge_constraint)

    def charge_constraint(model, t):
        "Maximum charge within a single hour"
        return model.Ein[t]*np.sqrt(eta) <= model.Rmax

    model.charge = Constraint(model.T, rule=charge_constraint)

    def positive_charge(model, t):
        'Limit discharge to the amount of charge in battery, including losses'
        return model.Eout[t] <= model.S[t]
    model.positive_charge = Constraint(model.T, rule=positive_charge)

    # Define the battery income, expenses, and profit
    income = sum(df.loc[t, 'Settlement Point Price' ] * model.Eout[t]*np.sqrt(eta) for t in model.T)
    expenses = sum(df.loc[t, 'Settlement Point Price'] * model.Ein[t] for t in model.T)
    profit = income - expenses
    model.objective = Objective(expr=profit, sense=maximize)

    # Solve the model
    solver = SolverFactory('glpk')
    solver.solve(model)

    hours = range(model.T[first_model_hour + 1], model.T[last_model_hour + 1] + 1)
    Ein = [value(model.Ein[i]) for i in hours]
    Eout = [value(model.Eout[i]) for i in hours]
    Price = [model.P.extract_values()[None][i] for i in hours]
    charge_state = [value(model.S[i]) for i in hours]

    df_dict = dict(
        hour=hours,
        Ein=Ein,
        Eout=Eout,
        price=Price,
        charge_state=charge_state
    )

    results_df = pd.DataFrame(df_dict)

    results_df['timeStamp'] = df.index

    return results_df

eta = 0.85 # Round trip storage efficiency
results_df = optimizeStorage(df,first_model_hour=0, last_model_hour=8759, setup = sp2)
results_df.to_csv('optimization_resultscase2.csv')

results_df['revenue'] = results_df.Eout*np.sqrt(eta)* results_df.price - results_df.Ein * results_df.price
total_revenue = results_df.revenue.sum()

serviceLife = 10
capex = sp2['Storage System Cost']*sp2['Storage Energy']*1000/serviceLife

print('Annual revenue was ${:,.2f}'.format(total_revenue))
print('Annual cost ${:,.2f}'.format(capex))
```

    WARNING: DEPRECATED: The default domain for Param objects is 'Any'.  However,
        we will be changing that default to 'Reals' in the future.  If you really
        intend the domain of this Param (P) to be 'Any', you can suppress this
        warning by explicitly specifying 'within=Any' to the Param constructor.
        (deprecated in 5.6.9, will be removed in 6.0) (called from
        C:\Users\cd7\AppData\Local\Continuum\anaconda3\envs\py39\lib\site-
        packages\pyomo\core\base\indexed_component.py:707)
    WARNING: DEPRECATED: Using __getitem__ to return a set value from its
        (ordered) position is deprecated.  Please use at()  (deprecated in 6.1,
        will be removed in 7.0) (called from
        C:\Users\cd7\AppData\Local\Temp/ipykernel_1148/1590877512.py:59)
    Annual revenue was $6,910,461.23
    Annual cost $7,000,000.00

### Case 3: Calculate the projected backcast 2019 revenue of Solar + Storage configuration
Here, two settings are considered: (1) AC coupled (left), (2) DC coupled (right).

AC coupled  | DC coupled
- | -
 |

Additional Constraints:

For AC coupled:

$\forall t$, $0<=E_t^{SoutG}+E_t^{Sin}/\sqrt{\eta}<=AC_t$

For DC coupled:

$\forall t$, $0<=E_t^{SoutG}+E_t^{Sin}/\sqrt{\eta}<=DC_t$

$\forall t$, $0<=E_t^{SoutG}+E_t^{BoutG}*\sqrt{\eta}<=220$

```python
def optimizeSolStorage(df, first_model_hour=0, last_model_hour=8759, setup=sp3):
    df = df.loc[first_model_hour:last_model_hour, :]

    model = ConcreteModel()

    # Define model parameters
    model.T = Set(doc='hour of year', initialize=df.hour.tolist(), ordered=True)

    model.Rmax = Param(initialize=setup['Storage Capacity'],
                       doc='Max rate of power flow (MW) in or out')
    model.Smax = Param(initialize=setup['Storage Energy'], doc='Max storage (MWh)')
    model.P = Param(initialize=df['Settlement Point Price'].tolist(), doc='RTMP for each hour')
    eta = 0.85 # Round trip storage efficiency

    # Charge in from grid, discharge out to grid, and state of charge
    model.GinE = Var(model.T, domain=NonNegativeReals)
    model.EoutG = Var(model.T, domain=NonNegativeReals)
    model.S = Var(model.T, bounds=(0, model.Smax))

    # Solar output to grid
    # Solar output to storage
    model.SinE = Var(model.T, domain=NonNegativeReals)
    model.SoutG = Var(model.T, domain=NonNegativeReals)

    model.ACout = Param(initialize=df.SolarAC.tolist(),doc='Solar AC')

    #Set all constraints
    def storage_state(model, t):
        'Storage changes with flows in/out and efficiency losses'
        if t == model.T.first():
            return model.S[t] == 0

        else:
            return (model.S[t] == (model.S[t-1]
                                + (model.GinE[t-1] )
                                + (model.SinE[t-1] )
                                - (model.EoutG[t-1])))

    model.charge_state = Constraint(model.T, rule=storage_state)

    def discharge_constraint(model, t):
        "Maximum dischage within a single hour"
        return model.EoutG[t] <= model.Rmax

    model.discharge = Constraint(model.T, rule=discharge_constraint)

    def charge_constraint(model, t):
        "Maximum charge within a single hour"
        return (model.GinE[t]+model.SinE[t]) <= model.Rmax

    model.charge = Constraint(model.T, rule=charge_constraint)

    def positive_charge(model, t):
        'Limit discharge to the amount of charge in battery, including losses'
        return model.EoutG[t] <= model.S[t]
    model.positive_charge = Constraint(model.T, rule=positive_charge)

    def inverter_limit(model, t):
        'Limit inverter output'
        return model.SoutG[t] + model.SinE[t]/np.sqrt(eta)<=model.ACout.extract_values()[None][t]

    model.inverter_limit = Constraint(model.T, rule=inverter_limit)

    # Define the total income, expenses, and profit
    income = sum(df.loc[t, 'Settlement Point Price'] * (model.EoutG[t]*np.sqrt(eta) + model.SoutG[t]) for t in model.T)
    expenses = sum(df.loc[t, 'Settlement Point Price'] * model.GinE[t]/np.sqrt(eta) for t in model.T)
    profit = income - expenses
    model.objective = Objective(expr=profit, sense=maximize)

    # Solve the model
    solver = SolverFactory('glpk')
    solver.solve(model)

    hours = range(model.T[first_model_hour + 1], model.T[last_model_hour + 1] + 1)
    GinE = [value(model.GinE[i]) for i in hours]
    EoutG = [value(model.EoutG[i]) for i in hours]
    SinE = [value(model.SinE[i]) for i in hours]
    SoutG = [value(model.SoutG[i]) for i in hours]
    Price = [model.P.extract_values()[None][i] for i in hours]
    charge_state = [value(model.S[i]) for i in hours]

    df_dict = dict(
        hour = hours,
        GinE = GinE,
        EoutG = EoutG,
        SinE = SinE,
        SoutG = SoutG,
        price = Price,
        charge_state = charge_state
    )

    results_df = pd.DataFrame(df_dict)

    results_df['timeStamp'] = df.index

    return results_df

results_df = optimizeSolStorage(df,first_model_hour=0, last_model_hour=8759, setup = sp3)
results_df.to_csv('optimization_resultscase3AC.csv')

results_df['revenue'] = (results_df.EoutG*np.sqrt(eta)+ results_df.SoutG)* results_df.price - (results_df.GinE/np.sqrt(eta)) * results_df.price
total_revenue = results_df.revenue.sum()

serviceLifeStorange = 10
serviceLifeSolar = 30

capex = sp3['Storage System Cost']*sp3['Storage Energy']*1000/serviceLifeStorange + sp3['Solar Size']*sp3['Solar System Cost']*1000000/serviceLifeSolar

print('Annual revenue was ${:,.2f}'.format(total_revenue))
print('Annual cost ${:,.2f}'.format(capex))
```

    WARNING: DEPRECATED: The default domain for Param objects is 'Any'.  However,
        we will be changing that default to 'Reals' in the future.  If you really
        intend the domain of this Param (ACout) to be 'Any', you can suppress this
        warning by explicitly specifying 'within=Any' to the Param constructor.
        (deprecated in 5.6.9, will be removed in 6.0) (called from
        C:\Users\cd7\AppData\Local\Continuum\anaconda3\envs\py39\lib\site-
        packages\pyomo\core\base\indexed_component.py:707)
    WARNING: DEPRECATED: Using __getitem__ to return a set value from its
        (ordered) position is deprecated.  Please use at()  (deprecated in 6.1,
        will be removed in 7.0) (called from
        C:\Users\cd7\AppData\Local\Temp/ipykernel_1148/1794385809.py:78)
    Annual revenue was $49,533,183.51
    Annual cost $20,000,000.00

```python
def optimizeSolStorage(df, first_model_hour=0, last_model_hour=8759, setup=sp3):
    df = df.loc[first_model_hour:last_model_hour, :]

    model = ConcreteModel()

    # Define model parameters
    model.T = Set(doc='hour of year', initialize=df.hour.tolist(), ordered=True)

    model.Rmax = Param(initialize=setup['Storage Capacity'],
                       doc='Max rate of power flow (MW) in or out')
    model.Smax = Param(initialize=setup['Storage Energy'], doc='Max storage (MWh)')
    model.P = Param(initialize=df['Settlement Point Price'].tolist(), doc='RTMP for each hour')
    eta = 0.85 # Round trip storage efficiency

    # Charge in from grid, discharge out to grid, and state of charge
    model.GinE = Var(model.T, domain=NonNegativeReals)
    model.EoutG = Var(model.T, domain=NonNegativeReals)
    model.S = Var(model.T, bounds=(0, model.Smax))

    # Solar output to grid
    # Solar output to storage
    model.SinE = Var(model.T, domain=NonNegativeReals)
    model.SoutG = Var(model.T, domain=NonNegativeReals)

    model.DCout = Param(initialize=df.SolarDC.tolist(),doc='Solar DC')

    #Set all constraints
    def storage_state(model, t):
        'Storage changes with flows in/out and efficiency losses'
        if t == model.T.first():
            return model.S[t] == 0

        else:
            return (model.S[t] == (model.S[t-1]
                                + (model.GinE[t-1] )
                                + (model.SinE[t-1] )
                                - (model.EoutG[t-1])))

    model.charge_state = Constraint(model.T, rule=storage_state)

    def discharge_constraint(model, t):
        "Maximum dischage within a single hour"
        return model.EoutG[t] <= model.Rmax

    model.discharge = Constraint(model.T, rule=discharge_constraint)

    def charge_constraint(model, t):
        "Maximum charge within a single hour"
        return  (model.GinE[t]+model.SinE[t]) <= model.Rmax

    model.charge = Constraint(model.T, rule=charge_constraint)

    def positive_charge(model, t):
        'Limit discharge to the amount of charge in battery, including losses'
        return model.EoutG[t] <= model.S[t]
    model.positive_charge = Constraint(model.T, rule=positive_charge)

    def DC_limit(model, t):
        'Limit DC output'
        return (model.SoutG[t] + model.SinE[t]/np.sqrt(eta))<=model.DCout.extract_values()[None][t]

    model.DC_limit = Constraint(model.T, rule=DC_limit)

    def inverter_limit(model, t):
        'Limit inverter output'
        return model.SoutG[t] + model.EoutG[t]*np.sqrt(eta)<=setup['Inverter Capacity']

    model.inverter_limit = Constraint(model.T, rule=inverter_limit)

    # Define the total income, expenses, and profit
    income = sum(df.loc[t, 'Settlement Point Price'] * (model.EoutG[t]*np.sqrt(eta)+ model.SoutG[t]) for t in model.T)
    expenses = sum(df.loc[t, 'Settlement Point Price'] * model.GinE[t]/np.sqrt(eta) for t in model.T)
    profit = income - expenses
    model.objective = Objective(expr=profit, sense=maximize)

    # Solve the model
    solver = SolverFactory('glpk')
    solver.solve(model)

    hours = range(model.T[first_model_hour + 1], model.T[last_model_hour + 1] + 1)
    GinE = [value(model.GinE[i]) for i in hours]
    EoutG = [value(model.EoutG[i]) for i in hours]
    SinE = [value(model.SinE[i]) for i in hours]
    SoutG = [value(model.SoutG[i]) for i in hours]
    Price = [model.P.extract_values()[None][i] for i in hours]
    charge_state = [value(model.S[i]) for i in hours]

    df_dict = dict(
        hour = hours,
        GinE = GinE,
        EoutG = EoutG,
        SinE = SinE,
        SoutG = SoutG,
        price = Price,
        charge_state = charge_state
    )

    results_df = pd.DataFrame(df_dict)

    results_df['timeStamp'] = dti

    return results_df

results_df = optimizeSolStorage(df,first_model_hour=0, last_model_hour=8759, setup = sp3)

results_df.to_csv('optimization_resultscase3DC.csv')

results_df['revenue'] = (results_df.EoutG*np.sqrt(eta)+ results_df.SoutG)* results_df.price - (results_df.GinE/np.sqrt(eta)) * results_df.price
total_revenue = results_df.revenue.sum()

serviceLifeStorange = 10
serviceLifeSolar = 30

capex = sp3['Storage System Cost']*sp3['Storage Energy']*1000/serviceLifeStorange + sp3['Solar Size']*sp3['Solar System Cost']*1000000/serviceLifeSolar

print('Annual revenue was ${:,.2f}'.format(total_revenue))
print('Annual cost ${:,.2f}'.format(capex))

```

    WARNING: DEPRECATED: The default domain for Param objects is 'Any'.  However,
        we will be changing that default to 'Reals' in the future.  If you really
        intend the domain of this Param (DCout) to be 'Any', you can suppress this
        warning by explicitly specifying 'within=Any' to the Param constructor.
        (deprecated in 5.6.9, will be removed in 6.0) (called from
        C:\Users\cd7\AppData\Local\Continuum\anaconda3\envs\py39\lib\site-
        packages\pyomo\core\base\indexed_component.py:707)
    WARNING: DEPRECATED: Using __getitem__ to return a set value from its
        (ordered) position is deprecated.  Please use at()  (deprecated in 6.1,
        will be removed in 7.0) (called from
        C:\Users\cd7\AppData\Local\Temp/ipykernel_1148/2858984879.py:82)
    Annual revenue was $44,888,953.79
    Annual cost $20,000,000.00

##  Using Gurobipy

(1) for the standalone storage system, it is an optimization problem (energy arbitrage).

```python
import gurobipy as gp
from gurobipy import GRB
```

```python
# Parameters

nhours = 8760
StorageCapacity = 50
StorageEnergy = 200
rtm = df['Settlement Point Price'].tolist()
timeh = range(nhours)
```

```python
eta = 0.85
# Create a new model
m = gp.Model ("solarPlan")
# Create variables
eOut = m.addVars(timeh,  vtype = GRB.CONTINUOUS, name ="eout")
eIn = m.addVars (timeh,  vtype = GRB.CONTINUOUS , name ="ein")
soc = m.addVars (timeh, ub=StorageEnergy, vtype = GRB.CONTINUOUS, name ="soc")
```

    Set parameter Username
    Academic license - for non-commercial use only - expires 2022-11-17

## Set constraints

```python
start_state = m.addConstrs((soc[t] == 0 for t in timeh  if t==0),'SOCst')
storage_state = m.addConstrs(( soc[t] == (soc[t-1]
                                + (eIn[t-1] * np.sqrt(eta))
                                - (eOut[t-1])) for t in timeh[1:]),'SOC')
```

```python
discharge_state = m.addConstrs((eOut[t] <= StorageCapacity for t in timeh),'discharge_st')

charge_state = m.addConstrs( (eIn[t]* np.sqrt(eta) <= StorageCapacity for t in timeh),'charge_st')

positive_charge = m.addConstrs( (eOut[t] <= soc[t] for t in timeh),'poscharge')
```

```python
### Define the battery income, expenses, and profit
# Set objective
m.setObjective(sum(rtm[t]*(eOut[t]* np.sqrt(eta) - eIn[t]) for t in timeh), GRB.MAXIMIZE)
```

```python
m.optimize()
```

    Gurobi Optimizer version 9.5.0 build v9.5.0rc5 (win64)
    Thread count: 2 physical cores, 4 logical processors, using up to 4 threads
    Optimize a model with 35040 rows, 26280 columns and 70077 nonzeros
    Model fingerprint: 0x24ecae74
    Coefficient statistics:
      Matrix range     [9e-01, 1e+00]
      Objective range  [9e-03, 9e+03]
      Bounds range     [2e+02, 2e+02]
      RHS range        [5e+01, 5e+01]

    Concurrent LP optimizer: dual simplex and barrier
    Showing barrier log only...

    Presolve removed 17527 rows and 8 columns
    Presolve time: 0.08s
    Presolved: 17513 rows, 26272 columns, 52544 nonzeros

    Ordering time: 0.00s

    Barrier statistics:
     AA' NZ     : 2.627e+04
     Factor NZ  : 2.188e+05 (roughly 20 MB of memory)
     Factor Ops : 3.439e+06 (less than 1 second per iteration)
     Threads    : 1

                      Objective                Residual
    Iter       Primal          Dual         Primal    Dual     Compl     Time
       0  -2.44549874e+06  1.65837950e+10  2.24e+00 8.00e+03  4.64e+05     0s
       1  -6.19061436e+05  3.34729902e+09  8.26e-04 4.14e+03  5.91e+04     0s
       2   1.52888358e+06  5.01584905e+08  6.65e-05 4.44e+02  8.75e+03     0s
       3   3.41238010e+06  8.33040709e+07  6.20e-06 5.38e+01  1.38e+03     0s
       4   4.51771217e+06  2.54039651e+07  1.16e-06 1.01e+01  3.60e+02     0s
       5   5.16307406e+06  1.31414203e+07  3.83e-07 3.51e+00  1.37e+02     0s
       6   5.74310964e+06  9.14692281e+06  1.32e-07 1.20e+00  5.83e+01     0s
       7   6.45130883e+06  7.61464657e+06  2.83e-08 3.04e-01  2.00e+01     0s
       8   6.79246873e+06  7.12256329e+06  3.97e-09 6.39e-02  5.67e+00     0s
       9   6.86766982e+06  6.98618790e+06  1.12e-09 1.89e-02  2.03e+00     0s
      10   6.88719954e+06  6.94211519e+06  5.70e-10 6.53e-03  9.38e-01     0s
      11   6.89975217e+06  6.92611757e+06  2.48e-10 2.68e-03  4.51e-01     0s
      12   6.90746537e+06  6.91304313e+06  6.14e-11 1.69e-04  9.40e-02     0s
      13   6.91008706e+06  6.91122165e+06  6.92e-12 1.02e-05  1.93e-02     0s
      14   6.91044516e+06  6.91053624e+06  2.38e-11 5.66e-06  1.55e-03     0s
      15   6.91046114e+06  6.91046136e+06  3.85e-11 7.28e-12  3.52e-06     0s
      16   6.91046123e+06  6.91046123e+06  7.90e-14 3.64e-12  3.52e-09     0s

    Barrier solved model in 16 iterations and 0.41 seconds (0.20 work units)
    Optimal objective 6.91046123e+06

    Crossover log...

        8512 DPushes remaining with DInf 0.0000000e+00                 0s
           0 DPushes remaining with DInf 0.0000000e+00                 0s

           4 PPushes remaining with PInf 0.0000000e+00                 0s
           0 PPushes remaining with PInf 0.0000000e+00                 0s

      Push phase complete: Pinf 0.0000000e+00, Dinf 2.3875346e-12      0s

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
        5910    6.9104612e+06   0.000000e+00   0.000000e+00      0s

    Solved with barrier
    Solved in 5910 iterations and 1.43 seconds (0.22 work units)
    Optimal objective  6.910461233e+06

### Case 3 AC

```python
m = gp.Model ("solarPlanAC")
```

```python
# add additional Parameters
# solarCost = 1.35
# batteryCost = 350
ACout =  df.SolarAC.tolist()
```

```python
# Create variables
GInB = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS , name ="GinB")
SinB = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="SinB")
BoutG = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="BoutG")
SoutG = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="SoutG")
soc = m.addVars (timeh, lb=0, ub=StorageEnergy, vtype = GRB.CONTINUOUS, name ="soc")
```

### constraints

```python
start_state = m.addConstrs((soc[t] == 0 for t in timeh  if t==0),'SOCst')
storage_state = m.addConstrs(( soc[t] == (soc[t-1]
                                + (GInB[t-1]+SinB[t-1])
                                - (BoutG[t-1])) for t in timeh[1:]),'SOC')
```

```python
discharge_state = m.addConstrs((BoutG[t] <= StorageCapacity for t in timeh),'discharge_st')

charge_state = m.addConstrs( ((GInB[t]+SinB[t]) <= StorageCapacity for t in timeh),'charge_st')

positive_charge = m.addConstrs((BoutG[t] <= soc[t] for t in timeh),'poscharge')

AC_lim = m.addConstrs((SoutG[t]+SinB[t]/np.sqrt(eta) <= ACout[t]  for t in timeh),'poscharge')
```

```python
### Define the battery income, expenses, and profit
# Set objective
m.setObjective(sum(rtm[t]*(BoutG[t]*np.sqrt(eta)+ SoutG[t]-GInB[t]/np.sqrt(eta)) for t in timeh), GRB.MAXIMIZE)
m.optimize()
```

    Gurobi Optimizer version 9.5.0 build v9.5.0rc5 (win64)
    Thread count: 2 physical cores, 4 logical processors, using up to 4 threads
    Optimize a model with 43800 rows, 43800 columns and 105116 nonzeros
    Model fingerprint: 0x7599f007
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      Objective range  [9e-03, 1e+04]
      Bounds range     [2e+02, 2e+02]
      RHS range        [3e-07, 2e+02]

    Concurrent LP optimizer: dual simplex and barrier
    Showing barrier log only...

    Presolve removed 26287 rows and 17528 columns
    Presolve time: 0.14s
    Presolved: 17513 rows, 26272 columns, 52544 nonzeros

    Ordering time: 0.00s

    Barrier statistics:
     AA' NZ     : 2.627e+04
     Factor NZ  : 2.188e+05 (roughly 20 MB of memory)
     Factor Ops : 3.439e+06 (less than 1 second per iteration)
     Threads    : 1

                      Objective                Residual
    Iter       Primal          Dual         Primal    Dual     Compl     Time
       0   3.94819067e+07  1.73971051e+10  2.45e+01 1.02e+03  4.85e+05     0s
       1   4.19588304e+07  3.50998778e+09  1.01e-02 5.38e+02  6.12e+04     0s
       2   4.40442026e+07  6.09012084e+08  1.07e-03 6.51e+01  9.89e+03     0s
       3   4.59443758e+07  1.65241237e+08  8.61e-05 1.15e+01  2.08e+03     0s
       4   4.70064715e+07  7.23308758e+07  1.91e-05 1.88e+00  4.38e+02     0s
       5   4.76433648e+07  5.74428712e+07  6.50e-06 6.04e-01  1.69e+02     0s
       6   4.82741469e+07  5.25950964e+07  1.93e-06 2.19e-01  7.44e+01     0s
       7   4.88689518e+07  5.03478975e+07  8.00e-07 4.58e-02  2.52e+01     0s
       8   4.93376646e+07  4.97087290e+07  1.51e-07 6.17e-03  6.28e+00     0s
       9   4.94414263e+07  4.95974317e+07  6.62e-08 1.59e-03  2.62e+00     0s
      10   4.94821737e+07  4.95655261e+07  3.57e-08 6.21e-04  1.40e+00     0s
      11   4.95072719e+07  4.95511269e+07  1.75e-08 2.49e-04  7.37e-01     0s
      12   4.95195120e+07  4.95376543e+07  9.00e-09 2.52e-05  3.01e-01     0s
      13   4.95295695e+07  4.95349344e+07  2.28e-09 2.81e-12  8.91e-02     0s
      14   4.95328269e+07  4.95334922e+07  2.15e-10 9.59e-13  1.11e-02     0s
      15   4.95331759e+07  4.95331885e+07  3.18e-10 2.73e-12  2.06e-04     0s
      16   4.95331835e+07  4.95331835e+07  2.27e-11 9.09e-13  2.08e-07     0s
      17   4.95331835e+07  4.95331835e+07  1.10e-09 9.09e-13  1.15e-10     0s

    Barrier solved model in 17 iterations and 0.50 seconds (0.24 work units)
    Optimal objective 4.95331835e+07

    Crossover log...

        8512 DPushes remaining with DInf 0.0000000e+00                 1s
           0 DPushes remaining with DInf 0.0000000e+00                 1s

           1 PPushes remaining with PInf 0.0000000e+00                 1s
           0 PPushes remaining with PInf 0.0000000e+00                 1s

      Push phase complete: Pinf 0.0000000e+00, Dinf 0.0000000e+00      1s

    Iteration    Objective       Primal Inf.    Dual Inf.      Time
        5898    4.9533184e+07   0.000000e+00   0.000000e+00      1s

    Solved with barrier
    Solved in 5898 iterations and 1.52 seconds (0.26 work units)
    Optimal objective  4.953318351e+07

### Case 3 DC

```python
try:
    m = gp.Model ("solarPlanDC")
    # add additional Parameters
    DCout =  df.SolarDC.tolist()
    InverterCapacity = 220

    # Create variables
    GInB = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS , name ="GinB")
    SinB = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="SinB")
    BoutG = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="BoutG")
    SoutG = m.addVars(timeh, lb=0, vtype = GRB.CONTINUOUS, name ="SoutG")
    soc = m.addVars (timeh, lb=0, ub=StorageEnergy, vtype = GRB.CONTINUOUS, name ="soc")

    start_state = m.addConstrs((soc[t] == 0 for t in timeh  if t==0),'SOCst')
    storage_state = m.addConstrs(( soc[t] == (soc[t-1]
                                    + (GInB[t-1]+SinB[t-1])
                                    - (BoutG[t-1])) for t in timeh[1:]),'SOC')

    discharge_state = m.addConstrs((BoutG[t] <= StorageCapacity for t in timeh),'discharge_st')

    charge_state = m.addConstrs( ((GInB[t]+SinB[t])<= StorageCapacity for t in timeh),'charge_st')

    positive_charge = m.addConstrs((BoutG[t] <= soc[t] for t in timeh),'poscharge')

    dc_lim = m.addConstrs((SoutG[t]+SinB[t]/np.sqrt(eta) <= DCout[t]  for t in timeh),'poscharge')

    inverter_lim =  m.addConstrs((SoutG[t]+BoutG[t]*np.sqrt(eta) <= InverterCapacity  for t in timeh),'poscharge')

    ### Define the battery income, expenses, and profit
    # Set objective
    m.setObjective(sum(rtm[t]*(BoutG[t]*np.sqrt(eta)+ SoutG[t]-GInB[t]/np.sqrt(eta)) for t in timeh), GRB.MAXIMIZE)
    m.optimize()

except gp.GurobiError as e:
    print ('Error code' + str( e.errno ) + ':' + str( e ))
except AttributeError:
    print ('Encountered an attribute error')
```

    Gurobi Optimizer version 9.5.0 build v9.5.0rc5 (win64)
    Thread count: 2 physical cores, 4 logical processors, using up to 4 threads
    Optimize a model with 52560 rows, 43800 columns and 122636 nonzeros
    Model fingerprint: 0xdd3430de
    Coefficient statistics:
      Matrix range     [9e-01, 1e+00]
      Objective range  [9e-03, 1e+04]
      Bounds range     [2e+02, 2e+02]
      RHS range        [3e-07, 3e+02]

    Concurrent LP optimizer: dual simplex and barrier
    Showing barrier log only...

    Presolve removed 28786 rows and 13354 columns
    Presolve time: 0.16s
    Presolved: 23774 rows, 30446 columns, 67153 nonzeros

    Ordering time: 0.00s

    Barrier statistics:
     AA' NZ     : 3.879e+04
     Factor NZ  : 2.424e+05 (roughly 24 MB of memory)
     Factor Ops : 3.422e+06 (less than 1 second per iteration)
     Threads    : 1

                      Objective                Residual
    Iter       Primal          Dual         Primal    Dual     Compl     Time
       0   7.64605493e+07  6.95990520e+09  2.31e+02 3.17e+03  2.40e+06     0s
       1   2.88805208e+07  1.05995399e+10  1.59e+01 2.55e-11  2.87e+05     0s
       2   2.96155200e+07  3.44638768e+09  1.87e+00 8.99e-10  5.58e+04     0s
       3   3.34632931e+07  2.75387200e+08  8.72e-02 1.46e-11  3.50e+03     0s
       4   3.95651478e+07  7.62812921e+07  1.29e-02 3.81e-12  5.28e+02     0s
       5   4.19425567e+07  5.66781477e+07  4.22e-03 2.99e-12  2.12e+02     0s
       6   4.33265290e+07  5.05728386e+07  1.48e-03 4.97e-12  1.04e+02     0s

    Barrier performed 6 iterations in 0.40 seconds (0.15 work units)
    Barrier solve interrupted - model solved by another algorithm

    Solved with dual simplex
    Solved in 6346 iterations and 0.41 seconds (0.14 work units)
    Optimal objective  4.488895379e+07

```python

```

Solar farm Investment Study in Houston.ipynb


# Calculating the *steady* solution for $\eta$ and $\theta$ domain using BVP solvers
\begin{equation}\label{p1introhpa} \Psi _{\eta \eta \tau }  = \frac{1}{{R_s }}\Psi _{\eta \eta \eta \eta }  + \Psi _{\eta \eta \eta } \Psi _\theta   - \Psi _{\eta \eta \theta } \Psi _\eta \\
 \\
\quad \Psi(\eta=\mp 1) = 0, \qquad \Psi_\eta(\eta=\mp 1) = - sin\theta, \qquad
-1 \le \eta \le1, \quad 0 \le \theta \le \pi \end{equation}

```julia
using DifferentialEquations
using Plots
using Sundials
using BenchmarkTools
using Calculus
using DelimitedFiles
using Interpolations
using LaTeXStrings
using SparseArrays
using Debugger
#using LSODA
using LinearAlgebra
```

In this code the vector **u** is used to store **u** = $[\Psi ... \Psi_\eta...\Psi_{\eta\eta} ... \Psi_{\eta\eta\eta}]$ in a single 1-dimensional vector. The BVPSolver runs perfectly with the latter structure. However the BVPSolver has a few issues if a matrix M is used to store the derivatives of $\Psi$ in columns e.g. $M = [\Psi, \Psi_\eta,\Psi_{\eta\eta}, \Psi_{\eta\eta\eta}]$ this is why the matrix form is not used in this code, interesting the ODE solver works perfectly with the Matrix form $M$.

```julia
pyplot()
function Ψ_smallr(θ::Float64, η::Float64 ,r::Float64)
    return(-η*(η^2-1)*sin(θ)/2.0 + r*η*(η^2-1)^2.0*(2.0+η^2)*sin(2*θ)/560.0 +
        r^2.0*η*(η^2-1)^2*((-591 -2294*η^2 + 161*η^4 + 1428*η^6)*sin(θ) + 3.0*(423 + 166*η^2 - 553*η^4 - 84*η^6)sin(3*θ))/62092800)
end

function Ψ_smallr_d1(θ::Float64, η::Float64 ,r::Float64)
    return(-(3.0*η^2-1)*sin(θ)/2.0 + r*(7*η^6-9*η^2+2)*sin(2*θ)/560.0 +
        r^2.0*(3*(-197 - 1112*η^2 + 6930*η^4 - 2772*η^6 - 8085*η^8 + 5236*η^10)*sin(θ) + (1269 - 6120*η^2 - 6930*η^4 + 24948*η^6 - 10395*η^8 - 2772*η^10)*sin(3*θ))/62092800.0)
end

function  Ψ_smallr_d2(θ::Float64, η::Float64 ,r::Float64)
    return(-3.0*η*sin(θ) + r*(42*η^5-18*η)*sin(2*θ)/560.0 +
        r^2.0*(3*( - 2224*η + 27720*η^3 - 16632*η^5 - 64680*η^7 + 52360*η^9)*sin(θ) + (- 12240*η - 27720*η^3 + 149688*η^5 - 83160*η^7 - 27720*η^9)*sin(3*θ))/62092800.0)
end

function  Ψ_smallr_d3(θ::Float64, η::Float64 ,r::Float64)
    return(-3.0*sin(θ) + r*(210*η^4-18)*sin(2*θ)/560.0 +
        r^2.0*(3*( - 2224 + 83160*η^2 - 83160*η^4 - 452760*η^6 + 471240*η^8)*sin(θ) + (- 12240 - 83160*η^2 + 748440*η^4 - 582120*η^6 - 249480*η^8)*sin(3*θ))/62092800.0)
end

function Ψ_bigr(θ::Float64, η::Float64)
    return(sin(pi*η)*sin(θ)/pi)
end

function Ψ_bigr_d1(θ::Float64, η::Float64)
    return(cos(pi*η)*sin(θ))
end

function  Ψ_bigr_d2(θ::Float64, η::Float64)
    return(-sin(pi*η)*sin(θ)*pi)
end

function  Ψ_bigr_d3(θ::Float64, η::Float64)
     return(-cos(pi*η)*sin(θ)*pi^2)
end

"""This is the differential equation for the ``\\psi(\\eta)^{(0)}`` term in the series.
``\\Psi_{\\eta \\eta \\eta \\eta
}^{(n)} = R_s( \\Psi_{\\eta}^{(0)} \\Psi _{\\eta \\eta}^{(0)} - \\Psi ^{(0)} \\Psi _{\\eta \\eta
\\eta }^{(0)})``
"""
function steady_diffeq_psi!(dψ, ψ, p, η) # This works for the ODEsolver psi is a matrix
  #dψ[:,1] =  ψ[:, 2]
  #dψ[:,2] =  ψ[:, 3]
  #dψ[:,3] =  ψ[:, 4]
  #dψ[:,4] = (ψ[:,2].*(A*ψ[:,3])-ψ[:,4].*(A*ψ[:,1]))*p[1]
  r, j, A_local, psi0, DPsiDtheta, DPsietaetaDtheta    = p

  Psi = @view ψ[:,1]
  Psi_d1 = @view  ψ[:,2]
  Psi_d2 = @view  ψ[:,3]
  Psi_d3 = @view  ψ[:,4]

  DPsi4eta = @view dψ[:,4]
  DPsi3eta = @view dψ[:,3]
  DPsi2eta = @view dψ[:,2]
  DPsi1eta = @view dψ[:,1]

  mul!(DPsietaetaDtheta, A_local, Psi_d2)
  mul!(DPsiDtheta, A_local, Psi )

  @. DPsi1eta =  Psi_d1
  @. DPsi2eta =  Psi_d2
  @. DPsi3eta =  Psi_d3
  @. DPsi4eta = (Psi_d1*DPsietaetaDtheta - Psi_d3*DPsiDtheta)*r

end

function steady_diffeq_psi_bvp!(dψ, ψ, p, η) # This works for the BVPsolver psi a vector of vectors
  #p[6] Dpsi_eta_etaDtheta
  #p[5] DpsiDtheta
  #p[4] psi0 initial condition
  #p[3] is the matrix A
  #p[2] is the J value for the number of steps in the J direction
  #p[1] is the Reynolds number

  #dψ[1:j+1]       .= ψ[j+2:2*j+2]
  #dψ[j+2:2*j+2]   .= ψ[2*j+3:3*j+3]
  #dψ[2*j+3:3*j+3] .=  ψ[3*j+4:4*j+4]
  #dψ[3*j+4:4*j+4] = (ψ[j+2:2*j+2].*(p[3]*ψ[2*j+3:3*j+3])-ψ[3*j+4:4*j+4].*(p[3]*ψ[1:j+1])).*p[1]
   r, j, A_local, psi0, DPsiDtheta, DPsietaetaDtheta    = p

  Psietaetaeta = @view ψ[3*j+4:4*j+4] #View are non allocating and are very fast to work with.
  Psietaeta = @view ψ[2*j+3:3*j+3]
  Psieta = @view ψ[j+2:2*j+2]
  Psi = @view  ψ[1:j+1]
  DPsi4eta = @view dψ[3*j+4:4*j+4]
  DPsi3eta = @view dψ[2*j+3:3*j+3]
  DPsi2eta = @view dψ[j+2:2*j+2]
  DPsi1eta = @view dψ[1:j+1]

  #Enforcing the boundary conditions at θ=0 and θ=π
  Psi[1] = Psi[j+1] = 0.0

  mul!(DPsietaetaDtheta, A_local, Psietaeta)
  mul!(DPsiDtheta, A_local, Psi)

  #Fully expanded loops for the copying this makes the
  #@inbounds for i =1:j+1
  #      dψ[i] = ψ[j+1+i]
  #      dψ[j+1+i] = ψ[2*j+2+i]
  #      dψ[2*j+2+i] =  ψ[3*j+3+i]
  #end
  @. DPsi1eta = Psieta
  @. DPsi2eta = Psietaeta
  @. DPsi3eta = Psietaetaeta
  @. DPsi4eta = (Psieta*DPsietaetaDtheta - Psietaetaeta*DPsiDtheta)*r #This creates replaces = with .= and  * with .* and - with .- etc
end

function bc_bvp!(residual, u, p, η)  # psi[1] is the beginning of the etaspan, and psi[end] is the ending
    #p[4] is the initial condition psi0
    #p[3] is the matrix A
    #p[2] is the J value for the number of steps in the J direction
    j=p[2]
    @inbounds for i = 1:j+1
        residual[i] = u[1][i]         # The psi[1] (i.e,. psi^{0}) solution at the beginning of the time span should be 0
        residual[j+1+i] = u[1][j+1+i] - p[4][j+1+i]       #First derivative of  should be -1 at first time step
        residual[2*j+2+i] = u[end][i]       # the solution at the end of the time span should be 0
        residual[3*j+3+i] = u[end][j+1+i] - p[4][j+1+i]      #First derivative should be -1 at end time step
    end
end

```

    bc_bvp! (generic function with 1 method)

```julia
const Reynolds_number = 20.0
const J = 64  #Number of steps take in the theta direction from 0 to pi, restart kernel if changing this value
const Δθ = pi/J
const etaspan=(-1.0,1.0)

Dpsi_eta_eta_Dtheta = ones(J+1); #For the Boundary Value solver Dpsi_eta_eta_Dtheta can be declared as Float and not Real, mult! function can be unstable
Dpsi_Dtheta = ones(J+1);

high_order_method = false

if (high_order_method == true)
A =spdiagm(-2=>ones(J-1)/(12*Δθ) ,-1 => -8*ones(J-1)/(12*Δθ), 1 => 8*ones(J-1)/(12*Δθ),2 => -1*ones(J-1)/(12*Δθ))

A[2,1] = -1/(2*Δθ) #Second order theta derivatives near the boundary theta=0
A[2,3] =  1/(2*Δθ) #Second order theta derivatives near the boundary theta=0
A[2,4] = 0.0
A[end-1,end-3] = 0.0
A[end-1,end-2] = -1/(2*Δθ) #Second order theta derivatives near the boundary theta=pi
A[end-1,end] =  1/(2*Δθ) #Second order theta derivatives near the boundary theta=pi

A[1,1] = -3/(2*Δθ) #Second order theta derivatives near the boundary theta=0
A[1,2] =  4/(2*Δθ) #Second order theta derivatives near the boundary theta=0
A[1,3] = -1/(2*Δθ) #Second order theta derivatives near the boundary theta=0
A[end,end-2] = 1/(2*Δθ) #Second order theta derivatives near the boundary theta=pi
A[end,end-1] = -4/(2*Δθ) #Second order theta derivatives near the boundary theta=pi
A[end,end]   = 3/(2*Δθ) #Second order theta derivatives near the boundary theta=pi
else
    #First order accurate theta-derivative at theta=0 and theta=pi, second order elsewhere
A =spdiagm(-1 => -1*ones(J)/(2*Δθ), 1 => ones(J)/(2*Δθ))
A[1,1] = -1/Δθ
A[1,2] = 1/Δθ
A[end,end-1] = -1/Δθ
A[end,end] =1/Δθ
end

if (Reynolds_number < 40.0 )
    psi_eta = [-sin(j*Δθ) for j =0:J]
    psi_eta_eta=  [ Ψ_smallr_d2(j*Δθ, -1.0, Reynolds_number) for j =0:J]
    psi_eta_eta_eta=  [ Ψ_smallr_d3(j*Δθ, -1.0, Reynolds_number) for j =0:J]
else
    psi_eta = [-sin(j*Δθ) for j =0:J]
    psi_eta_eta=  [ Ψ_bigr_d2(j*Δθ, -1.0) for j =0:J]
    psi_eta_eta_eta=  [ Ψ_bigr_d3(j*Δθ, -1.0) for j =0:J]
end

```

    65-element Vector{Float64}:
      0.0
      0.3715488310966101
      0.7405958876748597
      1.1046218782050201
      1.4610735906812682
      1.8073496849668103
      2.1407897013917925
      2.4586672151661
      2.7581879484006553
      3.0364935096947803
      3.2906712687476984
      3.517770694200233
      3.7148262912374808
      ⋮
     -8.577149502577207
     -8.112340557115783
     -7.564792707851326
     -6.939276479300569
     -6.241583539078185
     -5.478459208591325
     -4.657519848646873
     -3.787156338897113
     -2.876425124141894
     -1.9349285285463325
     -0.9726862362802201
     -2.4310412866933147e-15

```julia
#The initial conditions psi0 (note here psi0 means psi@t=0) must be sent as a matrix

#This is the ODESolver variable psi0
#Uncomment the code below to solve the ODE IVP problem

#ψ0= hcat(zeros(J+1), psi_eta, psi_eta_eta , psi_eta_eta_eta)  #This works for the ODESolver AND BVPSolver
#ψ0 is declared as a Matrix
#p= (Reynolds_number, J, A, ψ0, Dpsi_Dtheta, Dpsi_eta_eta_Dtheta)
#prob = ODEProblem(steady_diffeq_psi!, ψ0, etaspan, p)
#sol = @time solve(prob, reltol=1e-5, alg_hints = [:stiff], Rosenbrock23())
#Dpsi_eta_eta_Dtheta = Array{Real,1}(undef,J+1) #This has to be declared as real type because stiff solvers output reals
#Dpsi_Dtheta  = Array{Real,1}(undef,J+1)

#This works fine for R < 10 with Vern 7 but there is a problem with  MethodError: no method matching Float64
#ψ0 is declared as a vector, Rosenbrock cannot handle VECTOR when called
#ψ0= vcat(zeros(J+1), psi_eta, psi_eta_eta , psi_eta_eta_eta)  #This works for the ODESolver AND BVPSolver
#Dpsi_eta_eta_Dtheta = Array{Real,1}(undef,J+1) #This has to be declared as real type because stiff solvers output reals
#Dpsi_Dtheta  = Array{Real,1}(undef,J+1)
#p= (Reynolds_number, J, A, ψ0, Dpsi_Dtheta, Dpsi_eta_eta_Dtheta)
#prob = ODEProblem(steady_diffeq_psi_bvp!, ψ0, etaspan, p)
#sol = @time solve(prob, reltol=1e-5, alg_hints = [:stiff], Vern7())

#This is the BVP solver, works with J =20 upto R=30 with small R bc, and upto R=60, 88 with bigR boundary conditions.
#ψ0 is declared as a VECTOR bvp only knows how to handle vectors

ψ0= vcat(zeros(J+1), psi_eta, psi_eta_eta , psi_eta_eta_eta)  #This works for the ODESolver AND BVPSolver
p= (Reynolds_number, J, A, ψ0, Dpsi_Dtheta, Dpsi_eta_eta_Dtheta)
bvp_psi_2point = TwoPointBVProblem(steady_diffeq_psi_bvp!, bc_bvp!, ψ0, etaspan, p)
sol = @time solve(bvp_psi_2point, alg_hints = [:stiff], GeneralMIRK4(),dt=0.05) #Very accurate solver
```

```julia
MatrixPsis=readdlm("psifin.m",Float64,);
size(MatrixPsis)

```

    (103, 256)

```julia
xinterval=range(-1,stop=1,length=101)
plot(xinterval,MatrixPsis[2:102,77])
```

```julia
theta_step=30
mypsi = [i[theta_step] for i in sol.u]
etarange = [i for i in sol.t]
plot(etarange, mypsi, linecolor=:orange)
plot!(x->Ψ_smallr((theta_step-1)*Δθ,x,Reynolds_number),-1,1,linestyle=:dot, linecolor=:blue)
plot!(title=L"R_s = "*string(Reynolds_number),xlabel=L"\eta",ylabel=L"\Psi", yguidefontrotation=-90)

```

```julia
theta_step=20

mypsi = [i[theta_step] for i in sol.u]
etarange = [i for i in sol.t]
plot(etarange, mypsi, linecolor=:orange)
plot!(x->Ψ_bigr((theta_step-1)*Δθ,x),-1,1, label="Large R analytical",  linestyle=:dash, linecolor=:red,xlabel = L"\eta", ylabel = L"\Psi",size=(800,400),yguidefontrotation=-90, legend=:outertopright)
plot!(x->Ψ_smallr((theta_step-1)*Δθ,x,Reynolds_number),-1,1, label="Small R analytical",  linestyle=:dot, linecolor=:blue,xlabel = L"\eta", ylabel = L"\Psi",size=(800,400),yguidefontrotation=-90, legend=:outertopright)
title!(L"\Psi\ at\ \theta = "*string((theta_step-1))*L"\pi/"*string(J))
```

```julia
#First derivative of psi wrt to eta at the theta=0 boundary
print(sol.u[1][2*J+3:3*J+3])

```

```julia
#First derivative of intitial psi wrt to eta at the theta=0 boundary
#print(ψ0[2*J+3:3*J+3])
#for i=2*J+3:3*J+3
for i=3*J+4:4*J+4
println(sol.u[1][i],"  ",  Ψ_bigr_d3(Δθ*(i-3*J-4), -1.0),"   ", Ψ_smallr_d3(Δθ*(i-3*J-4), -1.0, Reynolds_number),)
end
```

```julia
eta_vector = [i for i in sol.t]
eta_totalsteps = length(sol.u)
psi_sol =  zeros((J+1)*eta_totalsteps)

counter = 1
for j = 1:J+1
    for  matrix in sol.u
        psi_sol[counter] = matrix[j,1]
        counter += 1
    end
end

```

```julia
#sol.u[end][1:J+1]
mypsi
```

    142-element Vector{Float64}:
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
     -3.0871236125895066e-21
     -3.275098515194481e-21
      ⋮
     -6.367818716354477e-11
      2.326081311369809e-11
      1.8642284956060735e-11
      2.4070046430921664e-11
      1.0482558094629222e-11
      1.6322240467124378e-11
      2.209999090690901e-11
      1.2876849058615223e-10
      5.150075396586408e-9
      5.562007296376082e-9
      5.0167277390438815e-9
      7.525595730550375e-9

```julia
for plot_counter =1:5
theta_step = plot_counter
if (theta_step > J+1)
    println("The value of theta_step should be lower than "*string(J+2))
elseif (theta_step < 1)
    println("The value of theta_step should be greater than 0")
end
psi_attheta = @view psi_sol[ (eta_totalsteps*(theta_step-1) +1) : eta_totalsteps*theta_step ]
plot(eta_vector,psi_attheta,label="Julia numerical solution", size=(800,600), legend=:outertopright)
temp_plot =plot!(x->Ψ_smallr((theta_step-1)*Δθ,x,Reynolds_number),-1,1, label="Small R analytical",  xlabel = L"\eta", ylabel = L"\Psi",size=(800,400),yguidefontrotation=-90, legend=:outertopright)
title!(L"\Psi\ at\ \theta = "*string((theta_step-1))*L"\pi/"*string(J))
display(temp_plot)
sleep(1)
end
```

```julia
ψ0[:,1]
```

    101-element Vector{Float64}:
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     ⋮
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0
     0.0

```julia
length(sol.t)
```

    4

```julia
n=4
#eta_n     = length(sol.t)
#eta_values= [i for i in sol.t]
#theta_values=
#X = x'.*ones(n)
#y=10:15
#Y = y'.*ones(n)
z=[i for i =1:n^2]
#z= reshape(z,(n,n))
x = [j for j=1:n for i=1:n]
y = [i for j=1:n for i=1:n]

```

    16-element Vector{Int64}:
     1
     2
     3
     4
     1
     2
     3
     4
     1
     2
     3
     4
     1
     2
     3
     4

```julia
z
```

    4×4 Matrix{Int64}:
     1  5   9  13
     2  6  10  14
     3  7  11  15
     4  8  12  16

```julia
pyplot()
plot(x,y,z,seriestype=:scatter, markersize = 7,camera=(-30,30))
```

```julia
A
```

    11×11 SparseMatrixCSC{Float64, Int64} with 22 stored entries:
     -3.50141   3.50141    ⋅        ⋅      …    ⋅        ⋅        ⋅        ⋅
     -1.7507     ⋅        1.7507    ⋅           ⋅        ⋅        ⋅        ⋅
       ⋅       -1.7507     ⋅       1.7507       ⋅        ⋅        ⋅        ⋅
       ⋅         ⋅       -1.7507    ⋅           ⋅        ⋅        ⋅        ⋅
       ⋅         ⋅         ⋅      -1.7507       ⋅        ⋅        ⋅        ⋅
       ⋅         ⋅         ⋅        ⋅      …    ⋅        ⋅        ⋅        ⋅
       ⋅         ⋅         ⋅        ⋅          1.7507    ⋅        ⋅        ⋅
       ⋅         ⋅         ⋅        ⋅           ⋅       1.7507    ⋅        ⋅
       ⋅         ⋅         ⋅        ⋅         -1.7507    ⋅       1.7507    ⋅
       ⋅         ⋅         ⋅        ⋅           ⋅      -1.7507    ⋅       1.7507
       ⋅         ⋅         ⋅        ⋅      …    ⋅        ⋅      -3.50141  3.50141

```julia
plot(Dpsi_Dtheta)
```

```julia
plot(Dpsi_eta_eta_Dtheta)
```

```julia

```