Methodology

How balco.nyc calculates balcony solar production in NYC

balco.nyc estimates how much electricity, money, and CO₂ a plug-in solar panel could produce on any NYC balcony. The model combines NREL's PVWatts hourly simulation, a custom 3D shadow model of the surrounding block, NYC building data, and Con Edison residential rates. Expected accuracy is within ±12 to 18% of real-world annual output. This page documents every assumption, source, and formula.

Last updated:
Reading time: ~15 minutes
PVWatts V8 NYC PLUTO Con Edison SC-1 EPA eGRID

1Energy production model

Annual production is computed by NREL's PVWatts V8 API, which simulates 8,760 hourly performance values across a typical meteorological year using the NSRDB satellite weather dataset. A client-side fallback formula is used if PVWatts is unavailable.

1.1 Primary model: NREL PVWatts V8

PVWatts is the US Department of Energy's reference solar production model. balco.nyc calls it with parameters tuned for vertical, railing-mounted panels in an urban setting:

ParameterValueWhy
system_capacity0.4 to 1.6 kWFrom user's railing width (1 to 4 panels × 400W)
module_type1 (Premium)19% efficiency, better temperature coefficient
array_type0 (Fixed Open Rack)Balcony rails have open airflow; PVWatts models cell temperature from TMY weather under this setting, so no separate thermal multiplier is applied
tilt35°, 60°, 70°, or 90°90° = vertical railing, 70°/60° = angled mounts, 35° ≈ optimal for NYC latitude
azimuth0 to 315°User's balcony direction (8 compass points)
losses14%PVWatts default. Soiling is broken out into the monthly array below to avoid double-counting
dc_ac_ratio1.2Standard for small systems with micro-inverters
inv_eff96.5%Micro-inverter efficiency (Enphase IQ8 ~97%, budget ~96%)
datasetnsrdbSatellite-derived TMY data, best for US locations
soiling[3,3,4,5,6,7,7,7,6,5,4,3]Monthly soiling %, NYC urban profile calibrated to NREL, Sandia, and Fraunhofer urban-PV studies (3 to 7% range, summer-heavy from pollen)
albedo0.20Concrete balcony floor reflectance (light-painted walls would be ~0.30)
bifaciality0Monofacial panels (would be 0.75 for bifacial)
timeframemonthlyReturns 12-month production array

PVWatts outputs used:

  • ac_monthly: 12 values of monthly AC energy (kWh), used for the production chart and for applying per-month 3D shade factors.
  • ac_annual: total annual AC output (kWh), used when the shade model returns only an annual factor.

Losses decomposition

The 14% bundle approximates: mismatch 3%, wiring 4% (longer DC runs from balcony to junction than rooftop), connections 0.5%, light-induced degradation 1.5%, nameplate 1%, availability 3%, snow 1%. Soiling and shading are modeled separately. There is no rooftop-vs-balcony "balcony loss adder". The difference shows up in the soiling array and in the shade factor.

1.2 Fallback model (no API available)

When PVWatts is unavailable (no API key, network failure, no address entered), the calculator uses a client-side formula:

annual_kwh = BASELINE × system_kw × tilt_factor × azimuth_factor
             × urban_soiling × thermal_bonus × shade_factor

Where:

  • BASELINE = 1,300 kWh/kW/year. NYC PVWatts reference at optimal ~40° tilt with default losses. Aligned with the NYSERDA NY Solar Map published assumption of 1,238 kWh/kW/yr.
  • tilt_factor, calibrated against PVWatts vertical NYC and the HTW Berlin Stecker-Solar reference:
    • 35° → 1.00 (top-mount, near-optimal for NYC)
    • 60° → 0.85
    • 70° → 0.78
    • 90° → 0.60 (vertical railing)
  • azimuth_factor: S=1.00, SE/SW=0.92, E/W=0.72, NE/NW=0.45, N=0.32
  • urban_soiling = 0.95. Matches the annual average of the PVWatts soiling array.
  • thermal_bonus = 1.0. No double-count; PVWatts (and this fallback for consistency) treats cell-temperature effects as part of the baseline.
  • shade_factor: see Section 2.

Monthly distribution uses either NREL Solar Resource API data (location-specific GHI) or a hardcoded NYC seasonal curve:

Jan: 5.6%, Feb: 6.8%, Mar:  8.2%, Apr:  9.2%, May: 10.5%, Jun: 11.2%
Jul: 11.4%, Aug: 10.3%, Sep: 8.8%, Oct:  7.3%, Nov:  5.6%, Dec:  5.1%

2Shadow derating model

PVWatts assumes an unobstructed installation. NYC balconies face building-level shading that PVWatts cannot see. We apply a post-PVWatts shade multiplier computed either from a 3D model of the surrounding buildings (when available) or from a static lookup as a fallback.

2.1 3D shadow model (when 3D scene is loaded)

For addresses with NYC Building Footprints data, we build a local 3D scene of all buildings within 200m and run a per-month, per-half-hour irradiance-weighted shadow simulation against the user's balcony point.

Per-sample loop (computeAnnualShadeProfile, sampling every 30 min from sunrise to sunset for each of 12 representative days):

  1. Sun position: compute altitude and azimuth from SunPosition.calculate(month, minute) (Section 5).
  2. Irradiance weight = sin(altitude)^0.6. A clear-sky direct-irradiance proxy that gives a noon sample roughly 5× the weight of a dawn sample but doesn't over-spike at solar noon.
  3. Self-shading test: if the sun is behind the facade (|sun_azimuth − balcony_azimuth| > 90°), the panel only sees diffuse sky. We add a DIFFUSE_FRACTION = 0.30 contribution weighted by the irradiance weight. This corrects a v1 omission where self-shaded periods contributed zero. A vertical NYC facade still receives ~30% of its energy from diffuse sky.
  4. Direct-sun samples: when the sun is on the panel's side, weight the sample by irradiance_weight × cos(altitude) × cos(facade_diff) (panel-incidence cosine). This shifts shade math toward "what fraction of high-irradiance hours are blocked" rather than "what fraction of daylight".
  5. Neighbor blocking: for each non-target building, compute its angular span as seen from the balcony point (_polygonAngularSpan projects every polygon vertex onto the balcony's view azimuth). If the sun's azimuth falls inside that span and its altitude is below the angle subtended by the building top, the building is blocking. Severity = min(1, vertical_block × width_factor). We take the maximum across all neighbors.
  6. Aggregate: unshaded_sum += (1 − max_shadow) × sample_weight, total_weight += sample_weight.

Per-month shade factor = unshaded_sum / total_weight, clamped to [0.15, 0.98].

Annual shade factor = monthly factors weighted by the NYC GHI distribution (the same array used in §1.2).

Physics-vs-display separation. The colored shadow heatmap in the 3D scene uses display_score = min(1, physics_score × 1.8) for UI contrast only. Only physics_score ever feeds back into the energy calculation. This prevents the display amplification from leaking into kWh numbers.

Polygon-edge angular projection. Earlier versions of the model approximated each neighbor as "centroid + 10m". The actual polygon footprint is now projected to angular extents from the viewer, including correct wrap-around handling when a building straddles the ±π azimuth seam.

2.2 Static shade factor (fallback)

When the 3D scene isn't loaded (no footprint data, mobile path, or in-page calculator without WebGL), shade is interpolated from a continuous function:

ratio = floor / total_floors
base_exposure = 0.5 + 0.5 × tanh(3 × (ratio − 0.45))
shade_factor = range.min + base_exposure × (range.max − range.min)

Per-shading-environment ranges:

Shadingmin (low floors)max (top floors)
Open0.850.97
Some buildings0.650.94
Dense canyon0.450.87
Wide avenue0.700.96

The tanh sigmoid is centred at the 45th-percentile floor and is steepest in the middle of the building, so the floor-tier transition zones are smooth rather than step-functions.

2.3 Neighbor building query

When address data is available, we query NYC Building Footprints within a 200-meter radius. These footprints feed the 3D scene and the polygon-edge shadow projection. Without them, the calculator falls back to the static shade factor above.

2.4 Final energy formula

final_kwh = pvwatts_ac_annual × shade_factor       (uniform shade case)
final_kwh = Σ(pvwatts_ac_monthly[i] × monthly_shade_factor[i])   (3D case)

THERMAL_BONUS is set to 1.0, so it doesn't appear above. The shade factor is the only post-PVWatts multiplier.

3Building orientation detection

When the user enters an address, we query NYC Building Footprints for the polygon geometry and detect facade directions.

Algorithm:

  1. Extract exterior ring coordinates from the building footprint polygon.
  2. Compute edge vectors (dx, dy) between consecutive vertices.
  3. Calculate each edge's compass bearing: atan2(dx, dy) converted to degrees.
  4. Compute perpendicular facade directions: edge_bearing ± 90°.
  5. Sort edges by length (longest edge = primary facade).
  6. Map facade directions to the nearest 45° compass increment.
  7. Rank by solar potential: S > SE/SW > E/W > NE/NW > N.
  8. Return the best solar-facing direction as the suggestion.

Confidence: "high" if the longest edge is more than 1.3× the second-longest; "medium" otherwise.

Manhattan grid note. Manhattan's street grid runs ~29° east of true north. A building that "faces the avenue" actually faces ~209° (SSW) or ~29° (NE). The algorithm reads this from the actual polygon, not from grid assumptions.

4Financial model

4.1 Electricity rate

Con Edison SC-1 residential rate: $0.34/kWh all-in marginal rate (supply + delivery + GRT + sales tax, excluding the flat customer charge). Sources: Con Edison historical bill table 2023 to 2025, projected forward by the 2026 PSC-approved rate-case settlement (+3.5% in 2026).

The marginal rate is the right number for solar offset: every kWh produced replaces one extra kWh the household would have bought. The Customer Charge is intentionally excluded because solar can't offset a flat fee.

4.2 Annual savings

annual_savings  = annual_kwh × 0.34
monthly_savings = annual_savings / 12
bill_offset_%   = annual_kwh / (monthly_bill / 0.34 × 12) × 100

Bill offset is clamped to a max of 100% and guarded against a $0 monthly bill (the consumption denominator is clamped to ≥1 kWh).

4.3 Payback period

Simple payback:

simple_payback = adjusted_cost / annual_savings

NPV payback runs inside the same 25-year loop as lifetime savings (below), interpolating within the crossover year for a fractional result.

4.4 25-year lifetime value

lifetime_savings = Σ(annual_kwh × (1 − degradation)^i × 0.34 × (1 + escalation)^i,  i=0..24)

Where:

  • degradation is tier-aware (default mid-tier 0.5%/yr), per the NREL 2024 PV degradation review:
    • Premium: 0.4%/yr → 90.5% of original output at year 25
    • Mid: 0.5%/yr → 88.7%
    • Budget: 0.7%/yr → 84.3%
  • escalation is user-selectable, default 3%/yr (mid), with low (2%) and high (4%) presets exposed in the Customize panel. Long-run national EIA data tracks ~2 to 2.5%/yr; recent Con Ed history is closer to 7%/yr but skewed by one-off settlements. 3% is a reasonable central estimate; the band conveys honest uncertainty.

4.5 System cost scaling

The user selects a cost tier (budget / mid / premium) calibrated to an 800W kit. For other system sizes, cost scales linearly:

adjusted_cost = tier_cost × (system_watts / 800)

Reference costs (800W complete kit, 2026 US retail):

TierCostNotes
Budget$1,200No-name panel + cheap micro-inverter
Mid$1,500EcoFlow PowerStream, Anker SOLIX entry
Premium$1,800Anker SOLIX RS40P, Bright Saver complete kit

The Federal Residential Clean Energy Credit (§25D) expired for expenditures after December 31, 2025 under P.L. 119-21. Cost figures are gross; no federal credit is netted out.

4.6 Offset assumption

Production is assumed to offset household consumption 1:1 at the marginal retail rate. For typical balcony users, production is well below consumption so there is no excess export to model. NY's SUNNY Act (S8512/A9111), which would formalize this for small plug-in solar, has not yet been enacted as of this writing.

5Sun position (NOAA simplified algorithm)

js/sun-position.js implements a simplified NOAA solar-position algorithm tuned for NYC.

  • Day of year uses the 15th of each month (DOY_TABLE) as the representative day.
  • Year is read dynamically from new Date().getFullYear() so the Julian-day base advances with time instead of drifting.
  • DST switch is keyed off day-of-year, not month: EDT (UTC−4) for DOY 67 to 304 (Mar 8 to Nov 1 in 2026), EST (UTC−5) otherwise. This avoids the off-by-week errors that month-based switching produced in early March and late October.
  • Azimuth is computed via atan2(sin_az, cos_az), robust at all altitudes, with no separate acos branch.

getDayBounds(month) searches for sunrise and sunset by scanning altitude crossings, used by the 3D shade simulation to bound its sampling loop.

6Environmental impact

CO₂ offset

co2_lbs = annual_kwh × 0.89

0.89 lbs CO₂/kWh, from the EPA eGRID2023 output emission rate for the NYCW subregion (released 2025, latest available). This is the "average grid" number, conservative relative to the eGRID non-baseload rate (~0.97 lbs/kWh).

Equivalencies

  • Trees: co2_lbs / 48, the EPA's averaged-across-all-trees figure for annual sequestration.
  • Driving miles offset: co2_lbs / 0.89, the EPA 2024 average passenger-car emission rate (0.906 lbs/mile rounded).
  • Smartphone charges: annual_kwh × 1000 / 12, ~12 Wh per full smartphone charge.

7Data pipeline

7.1 Address resolution

  1. Google Places Autocomplete: user types address, gets type-ahead suggestions bounded to NYC (40.48°N to 40.92°N, 74.26°W to 73.70°W).
  2. On selection, extract lat/lon, formatted address, and address components.

7.2 Building data lookup

NYC Geoclient runs first (its output BBL/BIN feeds the other queries), then three queries fire concurrently via Promise.allSettled():

a) NYC Geoclient → PLUTO

  • Parse address into houseNumber, street, borough (with a corrected USPS ZIP atlas: prefix 104 → Bronx; Long Island 110 is intentionally excluded as it isn't NYC).
  • Call NYC Geoclient (via server proxy for CORS), get BBL and BIN.
  • Query PLUTO by BBL, get numfloors, yearbuilt, bldgclass, unitsres, bldgarea, zonedist1.

b) NYC Building Footprints

  • Query by BIN, get polygon, heightroof, groundelev.
  • Run orientation detection (Section 3), suggest a balcony direction.

c) NREL Solar Resource

  • Query by lat/lon, get monthly GHI.
  • Normalize into a per-month distribution.

d) Neighbor query (background, non-blocking)

  • 200m radius, up to 500 buildings, feeds the 3D scene and shadow model.

7.3 Form pre-fill

Auto-populated from building data:

  • Total floors ← PLUTO numfloors
  • Direction picker ← footprint orientation algorithm
  • Building info card ← address, floors, year built, building class, height

7.4 User-adjustable inputs (Customize panel)

The breakdown modal exposes the previously hardcoded modeling assumptions:

InputOptionsDefault
Mount tilt35° / 60° / 70° / 90°90°
System size400W / 800W / 1200W / 1600W800W
Equipment tierBudget / Mid / PremiumMid
Surrounding shadingOpen / Some / Dense / Wide avenueSome
Monthly electric bill$20 to $800$200
Rate escalationLow (2%) / Mid (3%) / High (4%)Mid

7.5 Energy calculation

  1. Map form inputs to PVWatts parameters.
  2. Call PVWatts V8 API (or use the fallback formula).
  3. If a 3D scene is initialized, apply per-month shade factors; otherwise apply the static shade factor.
  4. Run the financial model with the selected tier and escalation preset.
  5. Compute environmental impact.

7.6 Graceful degradation

Every API has a fallback. The calculator works in full manual mode with zero API calls.

API failureFallback behavior
Google Places unavailable"Skip" link, manual entry
Geoclient failsQuery PLUTO by address string
PLUTO failsSliders keep defaults
Footprints failUser picks direction manually; no 3D shade
PVWatts failsClient-side fallback formula (Section 1.2), yellow banner shown
Solar Resource failsHardcoded NYC monthly distribution
Neighbor query failsNo 3D shade; static shade factor used

8Data sources

Every number in the calculator traces to one of the public datasets below.

SourceUsed for
NREL PVWatts V8
developer.nrel.gov
Hourly-simulated production
NREL Solar Resource
developer.nrel.gov
Monthly GHI irradiance
NYSERDA NY Solar Map
nysolarmap.com
Yield baseline cross-check (1,238 kWh/kW/yr)
NYC PLUTO
data.cityofnewyork.us
Building floors, class, year, units
NYC Building Footprints
data.cityofnewyork.us
Polygon, height, elevation, neighbor query
NYC Geoclient
api.nyc.gov
BBL and BIN from address
Google Places
developers.google.com
Address autocomplete and geocoding
Con Edison SC-1 historical bills
coned.com
Electricity rate
Con Edison 2026 to 2028 rate case
dps.ny.gov
Rate escalation
EPA eGRID2023
epa.gov
CO₂ factor (NYCW subregion)
HTW Berlin Stecker-Solar Simulator
solar.htw-berlin.de
Vertical-mount yield calibration
NY SUNNY Act (S8512/A9111)
nysenate.gov
Plug-in solar regulatory status
NREL 2024 PV Degradation Review
nrel.gov
Degradation rates by tier

9Accuracy & limitations

Expected accuracy:

  • ±12% on annual production with PVWatts V8 and the 3D shadow model active
  • ±18% with the client-side fallback (no PVWatts, no 3D)

What the model captures:

  • Latitude-specific solar resource and seasonal variation (PVWatts NSRDB)
  • Vertical and near-vertical tilt production loss, calibrated against PVWatts and HTW Berlin
  • Azimuth-dependent production across all 8 compass directions
  • NYC urban soiling, in the right ballpark (3 to 7% monthly) rather than the older 11 to 17% over-derate
  • Floor-level shadow estimation with a smooth tanh response, plus full polygon-edge 3D shadowing when neighbor footprints are available
  • Diffuse-sky contribution to self-shaded periods (~30% of clear-sky)
  • Irradiance-weighted shadow sampling (a noon shadow costs more than a dawn shadow)
  • Tier-aware degradation and rate-escalation bands in the 25-year financial projection

What the model still does NOT capture:

  • Micro-shading from things not in the building footprint dataset, like trees, awnings, AC units, signage
  • Snow coverage in winter months (a vertical panel sheds quickly but not instantly)
  • Future electricity-rate trajectory, the single largest swing factor in lifetime savings (2% vs 4% escalation ≈ ±15% on lifetime $)
  • Bifacial panels and light-painted walls (albedo and bifaciality are currently fixed)

Validation anchors:

  • HTW Berlin Stecker-Solar Simulator: 800W vertical south at 52.5°N → ~500 kWh/yr; scaled to NYC's 40.7°N latitude (~+25% irradiance) → ~625 kWh/yr unshaded vertical south, which the fallback model now reproduces within ±5%.
  • NYSERDA NY Solar Map baseline: 1,238 kWh/kW/yr at optimal tilt; PVWatts with the calculator's parameters produces ~1,300 kWh/kW/yr for premium fixed-tilt NYC, within bounds.
Questions about the methodology? See the FAQ or open an issue on GitHub.
Try the calculator →