Past a $0.55 carbon fee, the town should build its own power
At $0.40 per kWh of grid power, the cheapest plan is to build almost nothing and buy from the grid. At $0.55 the optimizer switches to about 31,000 m² of panels and 19,000 kWh of storage, and the grid fills in only around the summer peak.
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.
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.
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
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.
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.
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.
Scenario
Loan
Grid price
Carbon fee
Panel area, m²
Storage, kWh
Cost, ¢/kWh
All rates halved
4.0%
$0.09
$0.10
147
280
19.00
All rates doubled
9.0%
$0.15
$0.40
147
280
55.09
Carbon fee $0.45
6.0%
$0.12
$0.45
13,236
2,681
56.61
Carbon fee $0.55
6.0%
$0.12
$0.55
31,298
19,163
58.98
Carbon fee $0.75
6.0%
$0.12
$0.75
32,452
20,023
60.52
Cheaper loan
4.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.
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.
Optimized cost of energy across the 200 scenarios.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
Carbon feer_o
0.83
Loan rater_γ
0.49
Grid pricer_u
−0.01
00.250.50.751
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.
Each sampled rate against the optimized cost, one dot per scenario.
How the design responds
Panel area clusters at the floor or the ceiling.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
LandThe 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.
StorageCompressed 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.
RiskIt 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