namespace Simulator { export class Vehicle { batteryCapacity: number; batteryEfficiency: number = 0.9; gridTransformerEfficiency: number = 0.85; solarPanelEfficiency: number = 0.15; solarPanelArea: number = 1.0; // in square meters emptyVehicleWeight: number = 80; // kg driverWeight: number = 60; // kg additionalWeight: number; // additional weight, not counting cyclist and empty vehicle weight, in kg motorConsumption(distance: number, ascendingElevation: number): number { const g = 9.8; let totalWeight = this.emptyVehicleWeight + this.driverWeight + this.additionalWeight; let potentialEnergy = totalWeight * g * ascendingElevation; // Ep = m*g*h (result in Joules) potentialEnergy = potentialEnergy / 3600; // convert joules to watt-hour // empirical measures let baseConsumption = 13; // in Wh/km let maxWeight = 300; // in kg let additionalConsumptionAtMaxWeight = 5; // in Wh/km (without accounting for ascending elevation, only accelerations and additional friction) let weightRelatedConsumption = MathUtils.clamp(totalWeight * additionalConsumptionAtMaxWeight / maxWeight, 0, additionalConsumptionAtMaxWeight); return distance * (baseConsumption + weightRelatedConsumption) + potentialEnergy; } solarPower(irradiance: number): number { // TODO: should decompose climate data in normal radiance (modulated by incident angle) and diffuse irradiance // TODO: should add a shadowing factor (the panel won't be always exposed to the sun) return irradiance * this.solarPanelArea * this.solarPanelEfficiency; } } export interface Outing { distance: number; // in km ascendingElevation: number; // in meters } export class OutingPlanning { constructor(public dailyDistance: number, public dailyAscendingElevation: number) { } getOuting(dayOfWeek: number, hourOfDay: number, outing: Outing) { let dailyRatio = 0; if(dayOfWeek >= 5) { // week end dailyRatio = hourOfDay == 10 ? 1.0 : 0.0; } else { // other week day dailyRatio = hourOfDay == 7 || hourOfDay == 15 ? 0.5 : 0.0; } outing.distance = dailyRatio * this.dailyDistance; outing.ascendingElevation = dailyRatio * this.dailyAscendingElevation; } } export interface SimulationResult { vehicle: Vehicle; batteryLevel: number[]; // Remaining energy in the battery over time (one entry per hour), in Wh gridChargeCount: number; cumulatedGridRechargeEnergy: number; // Cumulated energy added to the battery from the power grid, in Wh of battery charge (actual power grid consumption will be slightly higer due to losses) cumulatedSolarRechargeEnergy: number; // Cumulated energy added to the battery from the solar panel, in Wh of battery charge (actual generated power is slightly higher due to losses) totalProducedSolarEnergy: number; // Cumulated energy produced (used or unused), before accounting for the battery recharge efficiency. cumulatedMotorConsumption: number; // Cumulated energy consumed by the motor, in Wh. In this simulation, this is equal to the energy drawn from the battery. } export function simulate(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult { let result: SimulationResult = { vehicle: vehicle, batteryLevel: [], gridChargeCount: 0, cumulatedGridRechargeEnergy: 0, cumulatedSolarRechargeEnergy: 0, totalProducedSolarEnergy: 0, cumulatedMotorConsumption: 0 }; let remainingBatteryCharge = vehicle.batteryCapacity; let outing: Outing = { distance: 0, ascendingElevation: 0 }; for(let day = 0; day < 365; ++day) { for(let hour = 0; hour < 24; ++hour) { let hourIdx = day * 24 + hour; planning.getOuting(day % 7, hour, outing); let consumption = outing.distance > 0 ? vehicle.motorConsumption(outing.distance, outing.ascendingElevation) : 0; let production = vehicle.solarPower(solarIrradiance[hourIdx]) * 1.0; // produced energy in Wh is equal to power (W) multiplied by time (h) result.totalProducedSolarEnergy += production; let solarCharge = production * vehicle.batteryEfficiency; // TODO: we should keep a margin because real users will recharge before they reach the bare minimum required for an outing remainingBatteryCharge += solarCharge - consumption; let fullGridRecharge = false; if(remainingBatteryCharge > vehicle.batteryCapacity) { solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity; remainingBatteryCharge = vehicle.batteryCapacity; } else if(remainingBatteryCharge <= 0 || (day==364 && hour==23)) { // TODO: detect if battery capacity is too low for a single outing, abort simulation and display an explanation for the user fullGridRecharge = remainingBatteryCharge <= 0; let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge; remainingBatteryCharge += rechargeEnergy; result.cumulatedGridRechargeEnergy += rechargeEnergy; result.gridChargeCount += 1; } result.cumulatedMotorConsumption += consumption; result.cumulatedSolarRechargeEnergy += solarCharge; result.batteryLevel[hourIdx] = fullGridRecharge ? 0 : remainingBatteryCharge; } } return result; } }