Demo of main data structures and functions of Microgrids.py. Main steps are:
Also, at the end of this notebook, there is an interactive simulation using ipywidgets.
try: # Install microgrids package in JupyterLite (if run in JupyterLite)
import piplite
await piplite.install(['microgrids', 'ipywidgets'])
except ImportError:
pass
import numpy as np
from matplotlib import pyplot as plt
import microgrids as mgs
Read load and solar data:
Remarks:
from pathlib import Path
folder = Path('.')
datapath = folder / 'data' / 'Ouessant_data_2016.csv'
data = np.loadtxt(datapath, delimiter=',', skiprows=2, usecols=(1,2,4))
# Split load and solar data:
Pload = data[:,0] # kW
Ppv1k = data[:,1] / 1000; # convert to kW/kWp
wind_speed = data[:,2]; # m/s
# Calibrate wind speed data against a mast measurement
ws_gain = 1.059 # ratio of Mast's mean /PVGIS' mean
wind_speed = ws_gain*wind_speed
Display load data
fig, ax = plt.subplots(1,1, figsize=(6,2.5))
td = np.arange(len(Pload))/24 # time in days
ax.plot(td, Pload, label="load")
ax.grid(True)
ax.set(
title='Electricity consumption in Ushant island in 2016',
ylabel='kW',
xlabel='day of the year'
);
fig.tight_layout()
plt.show()
We use the generic wind power curve model WindPower.capacity_from_wind
to transforme wind speed time series into a capacity factor time series (normalized power). Its main parameter is the Turbine Specific Power (W/m²).
Wind turbine parameters fitted to an EWT 900 kW DW52:
S_D52 = np.pi * (52/2)**2 # rotor swept area m²
TSP_D52 = 900e3/S_D52 # W/m²
Cp_D52, α_D52 = 0.521, 3.1 # fitted or actual power curve.
cf_wind = mgs.WindPower.capacity_from_wind(wind_speed, TSP_D52, Cp_D52, 25, α_D52)
np.mean(cf_wind) # annual capacity factor
0.39699594733634364
Describe the Microgrid project and its components in data structure
Financial parameters like discount rate, as well as technical details like the timestep of input data.
lifetime = 25 # yr
discount_rate = 0.05
timestep = 1 # h
project = mgs.Project(lifetime, discount_rate, timestep)
Used as last recourse when there is not enough solar production and the battery is empty
power_rated_gen = 1800. # /2 to see some load shedding
fuel_intercept = 0.0 # fuel curve intercept (l/h/kW_max)
fuel_slope = 0.240 # fuel curve slope (l/h/kW)
fuel_price = 1. # fuel price ($/l)
investment_price_gen = 400. # initial investiment price ($/kW)
om_price_gen = 0.02 # operation & maintenance price ($/kW/h of operation)
lifetime_gen = 15000. # generator lifetime (h)
generator = mgs.DispatchableGenerator(power_rated_gen,
fuel_intercept, fuel_slope, fuel_price,
investment_price_gen, om_price_gen,
lifetime_gen
)
Used as a buffer between the solar production and the consumption
energy_rated_sto = 5000. # rated energy capacity (kWh)
investment_price_sto = 350. # initial investiment price ($/kWh)
om_price_sto = 10. # operation and maintenance price ($/kWh/y)
lifetime_sto = 15. # calendar lifetime (y)
lifetime_cycles = 3000 # maximum number of cycles over life (1)
# Parameters with default values
charge_rate = 1.0 # max charge power for 1 kWh (kW/kWh = h^-1)
discharge_rate = 1.0 # max discharge power for 1 kWh (kW/kWh = h^-1)
loss_factor_sto = 0.05 # linear loss factor α (round-trip efficiency is about 1 − 2α) ∈ [0,1]
battery = mgs.Battery(energy_rated_sto,
investment_price_sto, om_price_sto,
lifetime_sto, lifetime_cycles,
charge_rate, discharge_rate,
loss_factor_sto)
Used in priority to feed the load. PV is proportional to the irradiance data the previous section
power_rated_pv = 3000. # rated power (kW)
irradiance = Ppv1k # global solar irradiance incident on the PV array (kW/m²)
investment_price_pv = 1200. # initial investiment price ($/kW)
om_price_pv = 20.# operation and maintenance price ($/kW)
lifetime_pv = 25. # lifetime (y)
# Parameters with default values
derating_factor_pv = 1.0 # derating factor (or performance ratio) ∈ [0,1]"
photovoltaic = mgs.Photovoltaic(power_rated_pv, irradiance,
investment_price_pv, om_price_pv,
lifetime_pv, derating_factor_pv)
Display PV production time series (which is proportional to the rated power of the plant power_rated_pv
)
fig, ax = plt.subplots(1,1, figsize=(6,2.5))
ax.plot(td, photovoltaic.production(), "C1")
ax.grid(True)
ax.set(
title=f'Production of a {power_rated_pv:.0f} kW PV plant in Ushant',
ylabel='kW',
xlabel='day of the year'
);
fig.tight_layout()
plt.show()
Used in priority to feed the load along PV.
The simple wind power model use the fixed capacity factor model derived from wind speed in the previous section
power_rated_wind = 900. # rated power (kW)
investment_price_wind = 3000. # initial investiment price ($/kW)
om_price_wind = 60.# operation and maintenance price ($/kW)
lifetime_wind = 25. # lifetime (y)
windgen = mgs.WindPower(power_rated_wind, cf_wind,
investment_price_wind, om_price_wind,
lifetime_wind)
Display wind power time series:
fig, ax = plt.subplots(1,1, figsize=(6,2.5))
td = np.arange(len(Pload))/24 # time in days
ax.plot(td, windgen.production(), "C4")
ax.grid(True)
ax.set(
title=f'Production of {power_rated_wind:.0f} kW of wind power in Ushant',
ylabel='kW',
xlabel='day of the year'
);
fig.tight_layout()
plt.show()
the Microgrid
data structure groups:
microgrid = mgs.Microgrid(project, Pload,
generator, battery, {
'Solar': photovoltaic,
'Wind': windgen
}
)
Display the microgrid structure and ratings
mgs.plotting.plot_ratings(microgrid, 'MW', xlim=(-3.5,2.5), ylim=(-2.5,2.5))
plt.show()
Simulation is done in two stages:
OperationStats
data structureMicrogridCosts
data structuresim_operation
)¶Simulation without trajectories, just to get the operation statisics
oper_stats = mgs.sim_operation(microgrid)
%timeit mgs.sim_operation(microgrid) # 112 ms in JupyterLite, 21 ms with regular Jupyter/Python 3.11 kernel
27.5 ms ± 2.35 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Simulation with trajectories (about twice slower)
oper_traj = mgs.TrajRecorder()
oper_stats = mgs.sim_operation(microgrid, oper_traj)
%timeit mgs.sim_operation(microgrid, oper_traj) # 190 ms in JupyterLite, 41 ms with regular Jupyter/Python kernel
47.9 ms ± 4.41 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
sim_economics
)¶mg_costs = mgs.sim_economics(microgrid, oper_stats)
%timeit mgs.sim_economics(microgrid, oper_stats) # 140 µs in JupyterLite, 34 µs with regular Jupyter/Python kernel
55.3 µs ± 4.94 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
Operation statistics are in oper_stats
(OperationStats
data structure)
print(f'Load shedding rate: {oper_stats.shed_rate:.1%}')
print(f'Renewable rate: {oper_stats.renew_rate:.1%}')
Load shedding rate: 0.0% Renewable rate: 75.3%
The energy mix found in oper_stats
can be displayed graphically:
mgs.plotting.plot_energy_mix(microgrid, oper_stats)
plt.show()
economic performance in mg_costs
(MicrogridCosts
data structure)
print(f'Levelized Cost of Electricity: {mg_costs.lcoe:.3f} $/kWh')
print(f'Net Present Cost: {mg_costs.npc/1e6:.2f} M$ (over {project.lifetime} years at {project.discount_rate:.0%} discount rate)')
Levelized Cost of Electricity: 0.219 $/kWh Net Present Cost: 20.93 M$ (over 25 years at 5% discount rate)
The costs for all components and for all factors can be display in one compact table with costs_table
. The lower left corner is the Net Present Cost.
cmat, cmat_rows, cmat_cols = mg_costs.costs_table()
print(f'Cost matrix. columns: {cmat_cols}')
print(f' rows: {cmat_rows}')
print(np.round(cmat/1e6, 3)) # in M$
Cost matrix. columns: ['Investment', 'Replacement', 'O&M', 'Fuel', 'Salvage', 'Total by component'] rows: ['Generator', 'Storage', 'Solar', 'Wind', 'All components'] [[ 0.72 1.946 1.679 5.659 -0.103 9.902] [ 1.75 0.842 0.705 0. -0.172 3.124] [ 3.6 0. 0.846 0. -0. 4.446] [ 2.7 0. 0.761 0. -0. 3.461] [ 8.77 2.788 3.991 5.659 -0.275 20.933]]
Zoom to first week of January: high load, wind at maximum, few solar → battery often empty
mgs.plotting.plot_oper_traj(microgrid, oper_traj)
plt.xlim(0,7) #
plt.show()
Zoom to one week in summer: much solar → battery often full → spillage
mgs.plotting.plot_oper_traj(microgrid, oper_traj)
plt.xlim(150,157)
plt.show()
Witness the effect of changing the power of the Solar & Wind plants, the Generator and Battery energy capacity (needs ipywidgets).
from ipywidgets import interactive, fixed
def interactive_mg(power_rated_gen, power_rated_pv, power_rated_wind, energy_rated_sto):
"""Create Microgrid which includes Generator,
PV plant and Battery with given ratings"""
generator = mgs.DispatchableGenerator(power_rated_gen,
fuel_intercept, fuel_slope, fuel_price,
investment_price_gen, om_price_gen,
lifetime_gen
)
battery = mgs.Battery(energy_rated_sto,
investment_price_sto, om_price_sto,
lifetime_sto, lifetime_cycles,
charge_rate, discharge_rate,
loss_factor_sto)
photovoltaic = mgs.Photovoltaic(power_rated_pv, irradiance,
investment_price_pv, om_price_pv,
lifetime_pv, derating_factor_pv)
windgen = mgs.WindPower(power_rated_wind, cf_wind,
investment_price_wind, om_price_wind,
lifetime_wind)
microgrid = mgs.Microgrid(project, Pload,
generator, battery, {
'Solar PV': photovoltaic,
'Wind': windgen
}
)
return microgrid
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_oper_costs(power_rated_gen, power_rated_pv, power_rated_wind, energy_rated_sto):
microgrid = interactive_mg(power_rated_gen, power_rated_pv, power_rated_wind, energy_rated_sto)
oper_stats = mgs.sim_operation(microgrid)
mg_costs = mgs.sim_economics(microgrid, oper_stats)
return oper_stats, mg_costs
def interactive_energy_mix(Solar_power=0., Wind_power=0., Batt_energy=0.):
"""display energy mix with given ratings"""
microgrid = interactive_mg(power_rated_gen, Solar_power, Wind_power, Batt_energy)
# Simulate
oper_stats, mg_costs = cached_oper_costs(power_rated_gen, Solar_power, Wind_power, Batt_energy)
# Show some performance stats:
print(f'Load shedding: {oper_stats.shed_rate:.1%}')
print(f'Renewable: {oper_stats.renew_rate:.1%}')
print(f'(Spilled renewable: {oper_stats.spilled_rate:.1%})')
print(f'Levelized Cost of Electricity: {mg_costs.lcoe:.3f} $/kWh')
# Display energy mix
fig, (ax1, ax2) = plt.subplots(2,1, num=1, figsize=(6,6),
gridspec_kw=dict(height_ratios=(2,1)))
mgs.plotting.plot_ratings(microgrid, xlim=(-3.5,2.5), ylim=(-2.5,2.5), ax=ax1)
mgs.plotting.plot_energy_mix(microgrid, oper_stats, ax=ax2)
fig.tight_layout()
plt.show()
Experiment starting from zero Solar power, Wind power and zero Battery:
With appropriate settings, you should find a Levelized Cost of Electricity (LCOE) below 0.20 \$/kWh (while it is 0.35 \\$/kWh without Solar, Wind and Battery)
interactive(interactive_energy_mix, Solar_power=(0.0, 8e3, 500), Wind_power=(0.0, 3e3, 250), Batt_energy=(0.0, 10e3, 500))
interactive(children=(FloatSlider(value=0.0, description='Solar_power', max=8000.0, step=500.0), FloatSlider(v…
Experiment starting from undersize generator, zero PV and zero battery:
def interactive_trajectories(Gen_power = 500., Solar_power=0., Wind_power=0., Batt_energy=0., t_plot=60):
"""display trajectories with given ratings, zoomed at `t_plot`"""
microgrid = interactive_mg(Gen_power, Solar_power, Wind_power, Batt_energy)
# Simulate with trajectory recording
oper_traj = mgs.TrajRecorder()
oper_stats = mgs.sim_operation(microgrid, oper_traj)
mg_costs = mgs.sim_economics(microgrid, oper_stats)
# Show some performance stats:
print(f'Load shedding: {oper_stats.shed_rate:.1%}')
print(f'Renewable: {oper_stats.renew_rate:.1%}')
print(f'(Spilled renewable: {oper_stats.spilled_rate:.1%})')
print(f'Levelized Cost of Electricity: {mg_costs.lcoe:.3f} $/kWh')
# Display trajectories
mgs.plotting.plot_oper_traj(microgrid, oper_traj)
plt.xlim(t_plot, t_plot+7)
plt.show()
interactive(interactive_trajectories, Gen_power = (0.0, 2e3, 100),
Solar_power=(0.0, 8e3, 500), Wind_power=(0.0, 3e3, 250), Batt_energy=(0.0, 10e3, 500),
t_plot=(0.0, 365.-7, 1))
interactive(children=(FloatSlider(value=500.0, description='Gen_power', max=2000.0, step=100.0), FloatSlider(v…