Skip to main content
SAM evaluates solar, solar-plus-storage and standalone storage scenarios by customer segment. Its active sequence imports workbook data, builds scenarios, calculates customer economics, estimates adoption, competes options and writes deployment results.

Input and indexing contracts

The workbook’s Scenarios sheet selects segments. Technology sheets define system sizes, CAPEX, OPEX, maximum markets, incentives, profiles, financing and adoption parameters. Parameter sheets use Input; annual sheets use Year. Sector Selector and the newer storage-incentive sheets have optional handling; absence of other directly indexed sheets or required segment columns raises lookup errors. Each acquisition year has a lifetime cash-flow column, including time zero. Forecast arrays include an initial installed-system observation plus subsequent adoption years. The number of forecast positions, project-life rows and tariff years must align; tariff and cost series must cover late purchases’ lifetime slices. The CLI parses --inputs-folder but main passes the module’s configured workbook constant directly to import_data. Changing the flag does not change that imported path. Programmatic import accepts a path. This specification does not assume that the parsed CLI option is connected to execution.

Solar generation and compensation

For acquisition year yy, interval profile ghg_h and PV size KyK_y: Py,h=Kygh,Xy,h=max⁡(Py,h−Lh,0),Sy,h=Py,h−Xy,hP_{y,h}=K_yg_h,\quad X_{y,h}=\max(P_{y,h}-L_h,0),\quad S_{y,h}=P_{y,h}-X_{y,h} XX is export and SS self-consumption. Lifetime production multipliers are calculated with -npf.pv(r, age, 0, 1), equivalent to (1+r)−age(1+r)^{-age}; this is not algebraically identical to (1−r)age(1-r)^{age}. Net metering values exports and self-consumption at the selected time-dependent tariff before applying the lifetime multiplier and tax handling. Net billing distinguishes exported and consumed energy; feed-in compensation uses the selected contractual compensation branch. Tariff periods use month/hour masks and annual escalation series rather than a single annual blended price.

Capital, operating and financing cash flows

PV installation cost is size × CAPEX × (1+sales_tax)(1+sales\_tax). Storage cost includes storage kW × discharge-duration hours × CAPEX and applicable replacement costs. The solar-plus-storage branch explicitly handles zero, one or two storage replacements using future CAPEX positions and the storage-life interval. Cost coverage beyond the forecast is therefore required. Direct purchase, loan and other financing branches build different lifetime cash flows. Financing terms, down payments, interest, taxes, incentives and annual operation are not interchangeable. The cash-flow components are combined before payback and adoption; incentives must not be subtracted a second time in downstream analysis.

Exact financing branches

Let CyC_y be the installation cost for acquisition year yy, dd its downpayment fraction, nn payment duration and ryr_y rate. With positive principal PP, annual npf.pmt uses PMT(r,n,P)=−Pr/[1−(1+r)−n]PMT(r,n,P)=-Pr/[1-(1+r)^{-n}] for nonzero rr, and −P/n-P/n at zero rate.
  • Direct Purchase: financing is −Cy-C_y at time zero and zero in subsequent project-life rows.
  • Lease: time zero is −dleaseCy-d_{lease}C_y; the next nleasen_{lease} rows contain the lease payment. The financed principal is Cy(1−dlease)+∑t>nleaseOPEXy,tC_y(1-d_{lease})+\sum_{t>n_{lease}}OPEX_{y,t}, where these OPEX cash flows are negative. The code uses the OPEX slice after the lease duration in this principal. This is the implemented calculation, not a conventional lease valuation assumption.
  • Any other financing string enters the loan branch: time zero is −dloanCy-d_{loan}C_y; the next nloann_{loan} rows contain PMT(ry,nloan,Cy(1−dloan))PMT(r_y,n_{loan},C_y(1-d_{loan})). The rate is selected by acquisition-year label and customer type from Loan_Rate, rather than one scalar rate for all purchases.
The final financing frame is added, with missing cells treated as zero, to the project-life purchase frame. Payment durations can therefore extend the aligned output if they exceed project life. Operating cash flow is calculated separately; the lease-specific removal of early OPEX must be retained when reproducing the combined cash flows.

Storage applications and limitations

Storage revenues and demand-charge savings follow the selected application and technology branches. The self-consumption branch uses the first supplied storage-size row, converts kW × duration to energy capacity, charges from PV exports and discharges into imports subject to the kW limit. Its recurrence is: uh=min⁡(Xh,PES),vh=min⁡(Importh,PES),SOCh=clip⁡(SOCh−1+uh−vh,0,EES)u_h=\min(X_h,P_{ES}),\quad v_h=\min(Import_h,P_{ES}),\quad SOC_h=\operatorname{clip}(SOC_{h-1}+u_h-v_h,0,E_{ES}) This expression assumes the one-hour intervals used by the implementation. State of charge resets at the simulated year’s start. The recurrence does not include an explicit round-trip-efficiency multiplier. Actual delivered benefits are derived from changes in bounded state of charge, not the unconstrained requested flow. Do not describe this branch as an optimized tariff-arbitrage dispatch. Other application branches calculate peak/demand-response revenues separately and must not be silently added as unconstrained simultaneous uses.

Financial response and maximum market

For purchase and loan cases, the adoption response uses alternative payback PBalt=−NPV(costs)/mean(benefits)PB^{alt}=-NPV(costs)/mean(benefits), clipped to project life. Residential purchase-payback reporting instead locates the last upward zero crossing in cumulative cash flow and interpolates the crossing. Non-residential purchase-payback uses ln⁡2/ln⁡(1+IRR)\ln2/\ln(1+IRR) after clipping negative IRR to zero. Standalone storage scales clipped 15-year payback to the configured project life. my=exp⁡(A PByalt+B)m_y=\exp(A\,PB^{alt}_y+B) This exponential response is not additionally clipped to one in that branch. Coefficients must yield meaningful fractions. For other financing options, let rr be year-one net bill savings divided by the original bill. Then: m(r)={e3.93r−3.02,r<0Q(r),0<r≤1.50.99,r>1.50,r=0m(r)=\begin{cases}e^{3.93r-3.02},&r<0\\Q(r),&0<r\le1.5\\0.99,&r>1.5\\0,&r=0\end{cases} Q(r)=−0.105168269230765r5+0.875491695804174r4−2.39574227855474r3+2.28287077505825r2+0.167472319347325r+0.0490034965034988Q(r)=-0.105168269230765r^5+0.875491695804174r^4-2.39574227855474r^3+2.28287077505825r^2+0.167472319347325r+0.0490034965034988 The exact-zero result follows strict branch masks. nan_to_num is applied to the resulting financing-market fraction; this does not validate zero original bills or undefined financial metrics elsewhere.

Diffusion and pre-competition adoption

Set equivalent time initially from Time in Market, then advance it by one every forecast step, including when maximum market declines. The Bass fraction includes a two-period offset: Fy=1−e−(p+qy)(ty+2)1+(qy/p)e−(p+qy)(ty+2)F_y=\frac{1-e^{-(p+q_y)(t_y+2)}}{1+(q_y/p)e^{-(p+q_y)(t_y+2)}} None or No leaves qq unchanged. Low, Moderate and High use qy=q0(1+ay)q_y=q_0(1+ay) with a=0.005,0.01,0.02a=0.005,0.01,0.02 per array step. Other transformation strings raise an error. Candidate cumulative adoption is MymyFyM_ym_yF_y, with existing installations prepended. Apply a running maximum to keep the pre-competition series nondecreasing.

Competition and annualization

Competing scenarios share segment, solar compensation, incentive flag, ITC flag, financing type, system-size type, CAPEX type, rate escalation and capacity factor. For each group/year with candidate counts njn_j: nj′={njmax⁡k(nk)/∑knk,∑knk≠00,otherwisen'_j=\begin{cases}n_j\max_k(n_k)/\sum_kn_k,&\sum_kn_k\ne0\\0,&\text{otherwise}\end{cases} The group total is the largest candidate, not the sum of candidates. There is no post-competition running-maximum correction. Consequently one option’s cumulative series may decline as competitive shares change. Annual adoption is the first difference of post-competition cumulative adoption with an initial zero. Smooth interior annual values with the mean of previous/current/next; retain both endpoint values. Reconstruct smoothed cumulative adoption by cumulatively summing smoothed annual values after replacing its initial value with initial post-competition adoption. This is not smoothing cumulative adoption directly.

Output contract

PV capacity is smoothed post-competition cumulative adoption × that year’s system size, with the initial size prepended. Annual capacity is the difference of cumulative capacity, not necessarily installation count × a constant size. Storage annual/cumulative capacity uses the applicable storage size and adoption series. Results include adoption, deployment, production, self-consumption, incentives and debugger economics. Respect kW/MW and kWh/GWh conversions before aggregating outputs.

Verification examples and boundary cases

With 1,000 customers, m=0.4m=0.4 and F=0.25F=0.25, candidate adoption is 100. If preceding cumulative adoption is 120, the pre-competition running maximum retains 120. Competing candidates of 100 and 50 become 66.666… and 33.333…, summing to 100. No integer rounding is implied. Validate zero bills, nonpositive or undefined IRR, negative/zero cash-flow crossings, sufficient future cost coverage, declining competitive adoption, empty scenario groups and storage power/energy limits. Sensitivity and regression checks must preserve acquisition-year/lifetime indexing, not only a final cumulative total. Existing test coverage is narrower than the number of financial/application branches; a mathematically smooth forecast does not validate calibration or policy assumptions.