Hanna
Boudara
Solar Microgrid Project
CEE 251 · Uncertainty, Design and Optimization · Duke University

How much solar should a town of 2,000 build?

We sized a solar panel and storage microgrid in MATLAB, then tested it against 300 scenarios of uncertain loan rates, grid prices and carbon fees.

The project in three steps

The carbon fee decides the designPast about $0.55 per kWh, the optimizer builds a full solar and storage system and the grid supplies under 3% of demand.
Solar is all or nothingAcross the Monte Carlo runs, panel area lands at its floor or its ceiling. Storage is what gets tuned to each scenario.
41 to 71 ¢/kWhThe cost of energy for the chosen design in 90% of scenarios. The carbon fee explains most of the spread (r = 0.83).
Step 1

Sizing the problem

Hot Springs, North Carolina runs a real solar microgrid. We used it as the template for a town of 2,000 people, and first needed two inputs: how much energy the town uses, and how much land there is for panels.

Demand

Each resident draws between 500 and 1,000 W on average. We took the midpoint and scaled it to a full day for the whole town.

\[\bar P=\tfrac12\,(500+1000)=750\ \text{W/person}\] \[\begin{aligned}E_{\text{day}}&=0.75\ \text{kW}\times 24\ \text{h}\times 2000\\&=36{,}000\ \text{kWh}\end{aligned}\]

The brief asked for one full day of demand in reserve, so 36,000 kWh is also the storage target.

Site area

We traced the outline of the existing Hot Springs array on satellite imagery. The dark rows inside it are the shadows the panels cast, not panels, so we measured each shadow row and subtracted it.

\[\begin{aligned}A_{\text{site}}&=36{,}873\ \text{m}^2-\textstyle\sum\text{shadow rows}\\&=14{,}800\ \text{m}^2\end{aligned}\]

The shadows covered more than half of the outline. Keep the 14,800 m² in mind: several designs later want three to four times more land.

Step 2

The optimization model

The model has two design variables and three uncertain prices. For any choice of panel area and storage size, it simulates every hour of a year and returns the average cost of each kWh the town consumes.

\(A\)
solar panel area, m² (design variable)
\(C\)
storage capacity, kWh (design variable)
\(r_\gamma\)
construction loan interest rate, 4% to 9%
\(r_u\)
utility price of grid electricity, $0.09 to $0.15 per kWh
\(r_o\)
carbon offset fee on each kWh of grid power, $0.10 to $0.90 per kWh
\[\begin{aligned}\min_{A,\,C}\quad& J(A,C;\,r_\gamma,r_u,r_o)\\ \text{subject to}\quad& 150\le A\le 60{,}000\ \text{m}^2\\ & 280\le C\le 112{,}000\ \text{kWh}\\ & g_1\le 0,\quad g_2\le 0\end{aligned}\] \[J=\frac{L+M+(r_u+r_o)\,E_{\text{grid}}}{E_{\text{demand}}}\]

Here \(L\) is the yearly loan payment on the panels and storage (it grows with \(A\), \(C\) and \(r_\gamma\)), \(M\) the operating cost and \(E_{\text{grid}}\) the energy bought from the grid (it shrinks as \(A\) and \(C\) grow), so \(J\) is the cost of each kWh in cents. Every kWh bought from the grid pays both the utility price and the carbon fee, so those two prices push in the same direction. Building more panels and storage replaces those fees with loan payments, and the optimizer looks for the point where the two balance. We solved it with sequential quadratic programming (SQP), starting from 15,000 m² and 28,000 kWh.

Reading the Lagrange multipliers

In the first runs both variables ended on their lower bounds with positive multipliers, \(\lambda_A\approx 20.0\) and \(\lambda_C\approx 3.8\). A positive multiplier on a lower bound means the cost was still falling there: the optimizer would have built even less if the bound allowed it. The two inequality constraints had \(\lambda=0\) in every run. They never bound, so each design was set by the cost tradeoff and the variable bounds alone.

How energy is routed each hour

Inside the simulation, a fixed dispatch rule decides where each hour's energy comes from. Solar serves the town first, any surplus charges storage, storage covers a shortfall, and the grid supplies whatever is still missing.

\[P_{sd}=\min(P_s,\,P_d)\] \[P_{sx}=\min\!\Big(P_s-P_d,\;p_{\text{in}}C,\;\tfrac{s_{\max}C-E}{\eta}\Big)\quad\text{if }P_s>P_d\] \[P_{xd}=\min\!\Big(P_d-P_s,\;p_{\text{out}}C,\;\eta(E-s_{\min}C)\Big)\quad\text{if }P_d>P_s\] \[P_{ud}=P_d-P_{sd}-P_{xd}\]
\(P_{sd},\,P_{sx}\)
solar to demand, solar into storage
\(P_{xd},\,P_{ud}\)
storage to demand, grid (utility) to demand
\(E,\;\eta\)
energy currently stored, round-trip efficiency of the storage technology
\(p_{\text{in}},\,p_{\text{out}}\)
maximum charge and discharge rate per kWh of capacity
\(s_{\min},\,s_{\max}\)
lowest and highest allowed state of charge
Step 2 · Technology

The technology grid

Three panel types times four storage technologies gives 12 designs. We optimized each one at baseline rates: grid power at 12¢ per kWh, a carbon fee of 40¢ per kWh and a 6.5% loan. Choose a measure to shade the grid, and select a design for its details.

Li-ion = lithium-ion battery · PSH = pumped-storage hydro · CAES = compressed-air energy storage. “Over site” marks designs that need more than the 14,800 m² we measured.

What the grid shows

The cheapest designs don't fit. At these rates the lowest costs come from low-efficiency panels with pumped hydro (34.2¢) or compressed air (33.6¢). Both fill the 60,000 m² upper bound, about four times the site.

High-efficiency panels aren't built at all. Area and storage stay at their floors, and the cost is just the grid price plus the carbon fee: 12¢ + 40¢ ≈ 52¢. At baseline rates the expensive panels never pay for themselves.

Bulk storage wins every row. Pumped hydro and compressed air were the two cheapest storage options for every panel type.

Step 2 · Sensitivity

Where the design flips

The grid shows one set of prices. To find the tipping point, we held the grid price at 12¢ and the loan at 6%, and raised the carbon fee. Each plot shows a full year of hourly energy flows into the town, with summer in the middle.

Hourly energy flows over a year at a carbon fee of 55 cents per kWh
grid → demand solar → demand storage → demand net storage flow
Panel area
31,298m²
Storage
19,163kWh
Cost of energy
58.98¢/kWh

Storage, in cyan, now carries the night, and the grid only fills in around the summer peak.

Grid price 12¢, loan 6%.

Between $0.55 and $0.75 the fee rises by 20¢, but the cost of energy rises by only 1.5¢. Once the panels and storage are built, the town buys so little grid power that the fee hardly touches it.

Every single-run optimization

We also halved and doubled all three rates (clipping each to its range) and tried a cheaper loan. The two marked rows differ only in the loan rate.

ScenarioLoanGrid priceCarbon feePanel area, m²Storage, kWhCost, ¢/kWh
All rates halved4.0%$0.09$0.10
147
280
19.00
All rates doubled9.0%$0.15$0.40
147
280
55.09
Carbon fee $0.456.0%$0.12$0.45
13,236
2,681
56.61
Carbon fee $0.556.0%$0.12$0.55
31,298
19,163
58.98
Carbon fee $0.756.0%$0.12$0.75
32,452
20,023
60.52
Cheaper loan4.0%$0.15$0.40
29,779
16,792
49.76

At a 9% loan the optimizer builds nothing. At 4% it builds almost 30,000 m² of panels, and the cost of energy falls from 55 to 50¢. The carbon fee is the main lever, but close to the tipping point, cheaper financing alone can flip the design.

Step 3

Monte Carlo under uncertain prices

A single run answers one set of prices. We ran the optimizer under random prices in two stages: first to see which technology wins, then to measure how uncertain the cost of the chosen design is.

Stage 1 · Which technology wins (100 scenarios)

For each scenario we drew the three rates uniformly across their ranges, optimized all 12 panel and storage pairs, and kept the cheapest. That is 1,200 optimizations in total.

\(r_\gamma\sim\mathcal U(0.04,\,0.09)\)\(r_u\sim\mathcal U(0.09,\,0.15)\)\(r_o\sim\mathcal U(0.10,\,0.90)\)

The winning designs fell into three groups, set by the grid price and the carbon fee:

Both prices low

grid under ~13¢ and carbon fee under 55¢
  • About 150 m² of high-efficiency panels and 0.3 MWh of compressed-air storage
  • Solar covers only 8 to 10% of demand
  • Lowest cost of the three groups

One price high

grid over ~14¢ or carbon fee over 55¢
  • 55,000 to 60,000 m² of panels and 15 to 18 MWh of compressed-air storage
  • The grid supplies under 3% of demand
  • Higher cost, driven by the capital spend

Both prices high

grid near 15¢ and carbon fee past ~60¢
  • Panels at the 60,000 m² cap and more than 20 MWh of storage, usually pumped hydro
  • The grid supplies under 1% of demand
  • Highest cost: the town is nearly off-grid

Stage 2 · How uncertain is the cost (200 scenarios)

With the design fixed to high-efficiency panels and compressed-air storage, we drew each rate from a lognormal distribution, since real rates are positive and skewed to the right. Each one is centered on the middle of its range, with a spread that puts the ends of the range about two standard deviations out. The three rates were drawn independently.

\(\tilde r=\tfrac12\,(r_{\max}+r_{\min})\)\(c_r=\dfrac{r_{\max}-r_{\min}}{4\,\tilde r}\)\(R=I_3\)
RateRangeMedianCoefficient of variation
Loan rate4% to 9%6.5%0.19
Grid price$0.09 to $0.15$0.120.125
Carbon fee$0.10 to $0.90$0.500.40
Histogram of the optimized cost of energy across 200 Monte Carlo scenarios, centered between 45 and 60 cents per kWh
Optimized cost of energy across the 200 scenarios.
Cumulative distribution of the optimized cost of energy
Cumulative distribution of the same costs.

In 90% of scenarios the cost of energy falls between 41 and 71 ¢/kWh. Most land between 45 and 60¢, with a slight skew to the right: low financing and low carbon fees give costs in the low 40s, while expensive scenarios pass 70¢, about 75% more.

Which price drives the cost

The carbon fee dominates. As it rises, the optimizer pays more up front to avoid it, so the cost climbs steeply until the system is built out. The loan rate matters moderately, since it sets the cost of every panel and kWh of storage. The grid price barely registers, for two reasons. Its typical spread was only about ±1.5¢ around 12¢, against roughly ±20¢ around 50¢ for the carbon fee, and both apply to the same grid kWh. And whenever the carbon fee is high, the optimizer avoids the grid, which removes the grid price from the cost too.

Three scatter plots of each rate against the optimized cost: loan rate shows a moderate upward trend, grid price shows no trend, carbon fee shows a steep tight upward trend
Each sampled rate against the optimized cost, one dot per scenario.

How the design responds

Scatter of cost against optimized panel area, clustered at about 1,000 and 30,000 square meters
Panel area clusters at the floor or the ceiling.
Scatter of cost against optimized storage capacity, with a cluster at the floor and a wide spread between 10,000 and 20,000 kWh
Storage spreads out once it is built.

Panel area behaves like a yes-or-no decision. With the narrower Monte Carlo bounds, nearly every run ends at about 1,000 m² or about 30,000 m². A high carbon fee with a moderate loan rate means full investment; a low fee with an expensive loan means the floor and grid power.

Storage is the tuning knob. It often sits at its floor too, but when it is built it mostly lands between 10,000 and 20,000 kWh, sized to how much daytime solar has to be shifted into the night.

The final design

High-efficiency solar panels with compressed-air storage

Land The site has 14,800 m². High-efficiency panels produce the most energy per square meter, while the cheaper low- and standard-efficiency builds needed 43,000 to 60,000 m² in the technology grid.
Storage Compressed air was the cheapest or nearly cheapest storage for every panel type, and the usual pick in the Monte Carlo scenarios that built out solar.
Risk It is not the cheapest choice when carbon is cheap. It is built for the scenarios where carbon fees rise, which is where the cost risk is.

Next step. Our model gives surplus solar no value. If the town could sell excess energy back to the grid, even below the retail price, oversizing would stop being a penalty. That would push more designs toward a full build and make the cost less sensitive to the carbon fee and the loan rate.

MATLAB

Code

The MATLAB code behind the results. SolarMicro_opt runs the hourly simulation and the SQP optimization for one set of rates.

Control logic: dispatch inside the hourly simulation MATLAB
% use solar to meet demand when available; prioritize charging storage with any excess
if (Pd < Ps)
Psd = Pd;
Psx = min([Ps - Pd, max_P_I_X * cap_E_X, (maxCharge * cap_E_X - E) / Efficiency_X]);
% if solar falls short, use storage to cover the remaining demand
if (Pd > Ps)
    Psd = Ps;
Pxd = min([Pd - Ps, max_P_O_X * cap_E_X, (E - minCharge * cap_E_X) * Efficiency_X]);

%if solar + storage still cant meet demand, draw remaining energy from the grid
if Psd + Pxd < Pd
Pud = Pd - Psd - Pxd;
Technology sweep driver MATLAB
clear; clc;                             % clear command window

solarTypes   = 1:3;     % 1 = Low Efficiency, 2 = Standard Eff, 3 = High Eff
storageTypes = 1:4;     % 1 = Li-Ion, 2 = Flywheel, 3 = PSH, 4 = CAES

% Step 2: Define fixed financial rates (baseline case), we took the average of each range
utilRate = 0.12;      % $/kWh from the grid
ghgRate  = 0.40;      % $/kWh to offset GHG emissions
loanRate = 0.065;     % Interest rate on capital investment
rates    = [utilRate; ghgRate; loanRate];

% Step 3: Initialize results matrix
results = [];   % Each row will be: [storageType, solarType, cost, area, storage]

% Step 4: Loop through all combinations
for st = solarTypes
    for xt = storageTypes
        try
            % Call the optimization function
            [f_opt, ~, v_opt] = SolarMicro_opt(rates, 0, xt, st);
            results(end+1,:) = [xt, st, f_opt, v_opt(1), v_opt(2)];
        end
    end
end

% Step 5: Display
colNames = {'StorageType', 'SolarType', 'Cost_c_per_kWh', 'Area_m2', 'Storage_kWh'};
resultTable = array2table(results, 'VariableNames', colNames);

disp('=== Technology comparison ===');
disp(resultTable);
Monte Carlo, stage 1: 100 scenarios MATLAB
clear; clc;

N = 100;   % Number of iterations

solarTypes   = 1:3;     % 1=low, 2=standard, 3=high-eff
storageTypes = 1:4;     % 1=Li-Ion, 2=Flywheel, 3=PSH, 4=CAES

results = []; %starts with initialize the list

for i = 1:N
    % Random financial parameters in their valid ranges, choosing uniformly
    utilRate = 0.09 + rand() * (0.15 - 0.09);   % Uniform between 0.09 and 0.15
    ghgRate = 0.10 + rand() * (0.90 - 0.10);    % Uniform between 0.10 and 0.90
    loanRate = 0.04 + rand() * (0.09 - 0.04);   % Uniform between 0.04 and 0.09
    rates = [utilRate; ghgRate; loanRate];

        best_cost = inf;
        best_result = [];

        for st = solarTypes
            for xt = storageTypes
                try
                    [f_opt, ~, v_opt] = SolarMicro_opt(rates, 0, xt, st);

                    global avgPud_per_h % import global grid use
                    if f_opt < best_cost
                        best_cost = f_opt;
                        best_result = [utilRate, ghgRate, loanRate, st, xt, f_opt, v_opt(:)', avgPud_per_h];
                    end
                catch
                    fprintf('Error for combo xt=%d, st=%d\n', xt, st);
                    continue;
                end
            end
        end

    results(end+1,:) = best_result;
end

% Display the results
colnames = {'utilRate','ghgRate','loanRate','solarType','storageType','cost_c/kWh','Area_m2','Storage_kWh','GridUse_kWh/h'};
disp(array2table(results, 'VariableNames', colnames));
Monte Carlo, stage 2: 200 scenarios MATLAB
%solarMico_montecarlo.m

NMcs = 200;
%            loan utility ghg
rates_min = [0.04 ; 0.09 ; 0.10];
rates_max = [0.09 ; 0.15 ; 0.90];

rates_med = (rates_max +rates_min)/2 ;
rates_cov = (rates_max - rates_min)/4 ./ rates_med; %this might not be exact

R = [1.0 ,   0 , 0    ; % CORRELATION MATRIX, must be positive definite (when eigenvalues are positve), dont change the diagobal, maybe change the zeros
      0   , 1.0, 0     ;
      0   ,   0 , 1.0 ];

%make sure eigenvaluea are positve

if any (eig(R) <= 0)
    error('your correlation values make for a non positive definitie correlation matrix')
    return;
end

rates = corr_logn_rnd(rates_med, rates_cov, R, NMcs);       %3 x Nmcs

f_opt =zeros(1,NMcs);      %optimization cost cents / KW.h for each mcs
v_opt = zeros(2,NMcs);     %optimization design variable for each mcs



for k = 1:NMcs
   this_rate = rates(:,k);    % Get the k-th random rate set: [loanRate; utilRate; ghgRate]

     fprintf('Trial %d | Loan = %.4f | Utility = %.4f | GHG = %.4f\n', ...
             k, this_rate(1), this_rate(2), this_rate(3));

     [f_opt(k), g_opt, v_opt(:,k)] = SolarMicro_opt(this_rate, k);

     fprintf('         Cost = %.4f /kWh | Solar = %.1f m | Storage = %.1f kWh\n\n', ...
             f_opt(k), v_opt(1,k), v_opt(2,k));
end


save('SolarMicro_montecarlo.mat', 'f_opt', 'v_opt', 'rates');

%what next??

% find statistics for ...
%CDF of f_opt? CDF of v_opt(1)? CDF of v_opt(2)? plot CDFci (confidence
%intervals)
%correlation between f_opt and rates(3,:) ...
%etc
%the point is to understand how uncertainty changes the outcome, it gives
%out a bunch of optimal solutions