Youen Toupin
3 years ago
5 changed files with 196 additions and 52 deletions
@ -0,0 +1,50 @@
|
||||
input[type=number] { |
||||
-moz-appearance:textfield; |
||||
} |
||||
|
||||
.wide-label .field-label { |
||||
flex-grow: 2.5; |
||||
} |
||||
|
||||
.dropdown.is-fullwidth { |
||||
display: flex; |
||||
} |
||||
|
||||
.dropdown.is-fullwidth .dropdown-trigger, |
||||
.dropdown.is-fullwidth .dropdown-menu { |
||||
width: 100%; |
||||
} |
||||
|
||||
.dropdown-trigger.with-dropdown-icon::after { |
||||
border: 3px solid black; |
||||
border-radius: 2px; |
||||
border-right: 0; |
||||
border-top: 0; |
||||
content: " "; |
||||
display: block; |
||||
height: 0.625em; |
||||
margin-top: -0.4375em; |
||||
pointer-events: none; |
||||
position: absolute; |
||||
top: 50%; |
||||
right: 15px; |
||||
transform: rotate(-45deg); |
||||
transform-origin: center; |
||||
width: 0.625em; |
||||
} |
||||
|
||||
.climate-zone { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
svg g { |
||||
filter: drop-shadow( 4px 4px 3px rgba(0, 0, 0, .7)); |
||||
} |
||||
|
||||
.climate-zone:hover { |
||||
filter: brightness(1.2); |
||||
} |
||||
|
||||
svg text { |
||||
pointer-events: none; |
||||
} |
@ -0,0 +1,132 @@
|
||||
function clamp(x: number, mini: number, maxi: number) { |
||||
return x <= mini ? mini : (x >= maxi ? maxi : x); |
||||
} |
||||
|
||||
class Vehicle { |
||||
batteryCapacity: number; |
||||
batteryEfficiency: number = 1.0; // TODO: typical efficiency of a Li-ion battery (round-trip) is 90%
|
||||
|
||||
solarPanelEfficiency: number = 0.15; |
||||
solarPanelArea: number = 1.0; // in square meters
|
||||
|
||||
additionalWeight: number; // additional weight, not counting cyclist and empty vehicle weight, in kg
|
||||
|
||||
motorConsumption(distance: number, ascendingElevation: number): number { |
||||
// empirical measures
|
||||
let maxWeight = 200; // in kg
|
||||
let maxWeightAdditionalConsumption = 4; // in Wh/km
|
||||
let maxTestedElevation = 500; // in meters
|
||||
let maxTestedElevationConsumption = 7; // in Wh/m
|
||||
let baseConsumption = 14; // in Wh/km
|
||||
|
||||
let weightRelatedConsumption = clamp(this.additionalWeight * maxWeightAdditionalConsumption / maxWeight, 0, maxWeightAdditionalConsumption); |
||||
|
||||
// TODO: should not be multiplied by distance
|
||||
// TODO: should be multiplied by total vehicle weight
|
||||
let elevationRelatedConsumption = clamp(ascendingElevation * maxTestedElevationConsumption / maxTestedElevation, 0, maxTestedElevationConsumption); |
||||
|
||||
return distance * (baseConsumption + weightRelatedConsumption + elevationRelatedConsumption) |
||||
} |
||||
|
||||
solarPower(irradiance: number): number { |
||||
return irradiance * this.solarPanelArea * this.solarPanelEfficiency; |
||||
} |
||||
} |
||||
|
||||
interface Outing { |
||||
distance: number; // in km
|
||||
ascendingElevation: number; // in meters
|
||||
} |
||||
|
||||
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 == 8 || hourOfDay == 16 ? 0.5 : 0.0; |
||||
} |
||||
|
||||
outing.distance = dailyRatio * this.dailyDistance; |
||||
outing.ascendingElevation = this.dailyAscendingElevation; |
||||
} |
||||
} |
||||
|
||||
interface SimulationResult { |
||||
batteryLevel: number[]; // Remaining energy in the battery over time (one entry per hour), in Wh
|
||||
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)
|
||||
cumulatedMotorConsumption: number; // Cumulated energy consumed by the motor, in Wh. In this simulation, this is equal to the energy drawn from the battery.
|
||||
} |
||||
|
||||
interface SimulationParameters { |
||||
batteryCapacity: number, |
||||
additionalWeight: number, |
||||
climateZone: string, |
||||
dailyDistance: number, |
||||
dailyAscendingElevation: number |
||||
} |
||||
|
||||
function runSimulation(vehicle: Vehicle, solarIrradiance: number[], planning: OutingPlanning): SimulationResult { |
||||
let result: SimulationResult = { |
||||
batteryLevel: [], |
||||
cumulatedGridRechargeEnergy: 0, |
||||
cumulatedSolarRechargeEnergy: 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 = vehicle.motorConsumption(outing.distance, outing.ascendingElevation); |
||||
let production = vehicle.solarPower(solarIrradiance[hourIdx]) * 1.0; // produced energy in Wh is equal to power (W) multiplied by time (h)
|
||||
|
||||
let solarCharge = production * vehicle.batteryEfficiency; |
||||
|
||||
remainingBatteryCharge += solarCharge - consumption; |
||||
if(remainingBatteryCharge > vehicle.batteryCapacity) { |
||||
solarCharge -= remainingBatteryCharge - vehicle.batteryCapacity; |
||||
remainingBatteryCharge = vehicle.batteryCapacity;
|
||||
} |
||||
else if(remainingBatteryCharge < 0) { |
||||
let rechargeEnergy = vehicle.batteryCapacity - remainingBatteryCharge; |
||||
remainingBatteryCharge += rechargeEnergy; |
||||
result.cumulatedGridRechargeEnergy += rechargeEnergy; |
||||
} |
||||
|
||||
result.cumulatedMotorConsumption += consumption; |
||||
result.cumulatedSolarRechargeEnergy += solarCharge; |
||||
|
||||
result.batteryLevel[hourIdx] = remainingBatteryCharge; |
||||
}
|
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
function startSimulation(parameters: SimulationParameters) { |
||||
let climateData = (<any>window)['climate-zones-data.csv']; |
||||
|
||||
let vehicle = new Vehicle(); |
||||
vehicle.batteryCapacity = parameters.batteryCapacity; |
||||
vehicle.additionalWeight = parameters.additionalWeight; |
||||
let solarIrradiance: number[] = climateData[parameters.climateZone.toLowerCase()]; |
||||
let planning = new OutingPlanning(parameters.dailyDistance, parameters.dailyAscendingElevation); |
||||
|
||||
let simulationResult = runSimulation(vehicle, solarIrradiance, planning); |
||||
console.log(simulationResult); |
||||
} |
Loading…
Reference in new issue