diff --git a/src/Constants.tsx b/src/Constants.tsx index c2ad21b..1122eb6 100644 --- a/src/Constants.tsx +++ b/src/Constants.tsx @@ -66,6 +66,7 @@ export const LOCATIONS = { }, } as { [id: string]: LocationType }; export const OUTSKIRTS_WIND_MULTIPLIER = 2; // https://github.com/toddmedema/electrify/issues/96 +export const EQUATOR_RADIANCE = 1000; // at sea level, equator, clear day, noon https://en.wikipedia.org/wiki/Solar_irradiance export const TICK_MS = { PAUSED: 250, diff --git a/src/data/Weather.tsx b/src/data/Weather.tsx index 1382e98..896946d 100644 --- a/src/data/Weather.tsx +++ b/src/data/Weather.tsx @@ -1,4 +1,4 @@ -import { DAYS_PER_MONTH, DAYS_PER_YEAR } from "../Constants"; +import { DAYS_PER_MONTH, DAYS_PER_YEAR, EQUATOR_RADIANCE } from "../Constants"; import { DateType, RawWeatherType } from "../Types"; import { getRandomRange } from "../helpers/Math"; import { getSunriseSunset } from "../helpers/DateTime"; @@ -79,31 +79,48 @@ export function getWeather(date: DateType): RawWeatherType { }; } -// 0-1, percent of sun's energy hitting a unit of land relative to max at equator (~1360 w/m2) // Is later multiplied by cloudiness +// TODO should this just take in cloudiness? The game knows future weather... // TODO change to watts per sq meter or some fixed value, and verify that it's returning reasonably accurate values per location and season // (hoping that day length alone is a sufficient proxy / ideally don't need to make it any more complex) // https://earthobservatory.nasa.gov/features/EnergyBalance/page2.php // indicates a roughly linear correlation that each degree off from 0*N/S = 0.7% less sunlight -export function getRawSunlightPercent( +// TODO fix the pointiness, esp in shorter winter months - Maybe by factoring in day lenght to determine the shape of the curve? +// Day length / minutes from dark used as proxy for season / max sun height +// Rough approximation of solar output: https://www.wolframalpha.com/input/?i=plot+1%2F%281+%2B+e+%5E+%28-0.015+*+%28x+-+260%29%29%29+from+0+to+420 +// Potential more complex model for solar panels: https://pro.arcgis.com/en/pro-app/3.1/tool-reference/spatial-analyst/how-solar-radiation-is-calculated.htm +/** + * Calculates the raw solar irradiance in watts per square meter (W/m2) for a given date and location, not accounting for weather + * It first calculates the base irradiance based on the latitude, with a reduction factor for higher latitudes. + * It then gets the sunrise and sunset times for the given date and location. + * If the current time is between sunrise and sunset, it calculates the minutes from darkness (either sunrise or sunset, whichever is closer). + * It then calculates the irradiance based on a mathematical model that approximates the solar output as a bell curve. + * This model takes into account the time of day and the length of the day to approximate the height of the sun and the season. + * If the current time is outside of sunrise and sunset, it returns 0, indicating no solar irradiance. + * + * @param {DateType} date - The date and time to calculate the irradiance for. + * @param {number} lat - The latitude of the location to calculate the irradiance for. + * @param {number} long - The longitude of the location to calculate the irradiance for. + * @param {number} cloudCoverPercent - The percentage of cloud cover, from 0 to 100. + * @returns {number} - The calculated raw solar irradiance in W/m2. + */ +export function getRawSolarIrradianceWM2( date: DateType, lat: number, - long: number + long: number, + cloudCoverPercent: number ) { + let irradiance = EQUATOR_RADIANCE * (1 - 0.007 * Math.abs(lat)); // w/m2 + irradiance *= 1 - cloudCoverPercent / 100; const { sunrise, sunset } = getSunriseSunset(date, lat, long); if (date.minuteOfDay >= sunrise && date.minuteOfDay <= sunset) { const minutesFromDark = Math.min( date.minuteOfDay - sunrise, sunset - date.minuteOfDay ); - // TODO fix the pointiness, esp in shorter winter months - // Maybe by factoring in day lenght to determine the shape of the curve? - - // Day length / minutes from dark used as proxy for season / max sun height - // Rough approximation of solar output: https://www.wolframalpha.com/input/?i=plot+1%2F%281+%2B+e+%5E+%28-0.015+*+%28x+-+260%29%29%29+from+0+to+420 - // Solar panels generally follow a Bell curve - // Potential more complex model for solar panels: https://pro.arcgis.com/en/pro-app/3.1/tool-reference/spatial-analyst/how-solar-radiation-is-calculated.htm - return 1 / (1 + Math.pow(Math.E, -0.015 * (minutesFromDark - 260))); + return ( + irradiance / (1 + Math.pow(Math.E, -0.015 * (minutesFromDark - 260))) + ); } return 0; } diff --git a/src/helpers/Energy.tsx b/src/helpers/Energy.tsx index a5bd7a2..3ecbc48 100644 --- a/src/helpers/Energy.tsx +++ b/src/helpers/Energy.tsx @@ -1,4 +1,4 @@ -import { OUTSKIRTS_WIND_MULTIPLIER } from "../Constants"; +import { EQUATOR_RADIANCE, OUTSKIRTS_WIND_MULTIPLIER } from "../Constants"; export function getWindOutputFactor(windKph: number) { // Wind gradient, assuming 10m weather station, 100m wind turbine, neutral air above human habitation - https://en.wikipedia.org/wiki/Wind_gradient @@ -15,12 +15,16 @@ export function getWindOutputFactor(windKph: number) { return windOutputFactor; } -// Sunlight percent is a proxy for time of year and lat/long +// Since solar panel nameplate wattages are usually rated at peak output at equator noon, we use that as baseline // Solar panels slightly less efficient in warm weather, declining about 1% efficiency per 1C starting at 10C -// TODO what about rain and snow, esp panels covered in snow? +// TODO what about rain and snow, esp panels covered in snow? We should update irradianceWM2 based on weather when it's originally calculated... +// but that still means we'd need to track some additional historic value of "even though it's not currently snowing, they're still covered in snow" export function getSolarOutputFactor( - sunlightPercent: number, + irradianceWM2: number, temepratureC: number ) { - return sunlightPercent * Math.max(1, 1 - (temepratureC - 10) / 100); + return ( + (irradianceWM2 * Math.max(1, 1 - (temepratureC - 10) / 100)) / + EQUATOR_RADIANCE + ); } diff --git a/src/reducers/Game.tsx b/src/reducers/Game.tsx index fae99b4..e7a2cca 100644 --- a/src/reducers/Game.tsx +++ b/src/reducers/Game.tsx @@ -22,7 +22,7 @@ import { import { arrayMove } from "../helpers/Math"; import { getWindOutputFactor, getSolarOutputFactor } from "../helpers/Energy"; import { getFuelPricesPerMBTU } from "../data/FuelPrices"; -import { getRawSunlightPercent, getWeather } from "../data/Weather"; +import { getRawSolarIrradianceWM2, getWeather } from "../data/Weather"; import { dialogOpen, dialogClose, snackbarOpen } from "./UI"; import { DIFFICULTIES, @@ -530,9 +530,12 @@ function reforecastWeatherAndPrices(state: GameType): TickPresentFutureType[] { return { ...t, ...fuelPrices, - sunlight: - getRawSunlightPercent(date, state.location.lat, state.location.long) * - (weather.CLOUD_PCT / 100), + sunlight: getRawSolarIrradianceWM2( + date, + state.location.lat, + state.location.long, + weather.CLOUD_PCT + ), windKph: OUTSKIRTS_WIND_MULTIPLIER * weather.WIND_KPH, temperatureC: weather.TEMP_C, }; @@ -583,8 +586,6 @@ function updateSupplyFacilitiesFinances( now.temperatureC ); - console.log(now.minute, now.sunlight, solarOutputFactor); - // Pre-check how much extra supply we'll need to charge batteries let indexOfLastUnchargedBattery = -1; let totalChargeNeeded = 0;