reimplementation of the simulation in javascript (wip)
This commit is contained in:
parent
3121342337
commit
3bc782c9d6
50
simulator/src/app.scss
Normal file
50
simulator/src/app.scss
Normal file
@ -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;
|
||||||
|
}
|
132
simulator/src/simulator-core.ts
Normal file
132
simulator/src/simulator-core.ts
Normal file
@ -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);
|
||||||
|
}
|
@ -12,7 +12,7 @@
|
|||||||
<input name="battery-capacity" class="input" type="number" min="1" value="700"/>
|
<input name="battery-capacity" class="input" type="number" min="1" value="700"/>
|
||||||
</p>
|
</p>
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<a class="button is-static">kWh</a>
|
<a class="button is-static">Wh</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,7 +99,7 @@
|
|||||||
<div class="field-body">
|
<div class="field-body">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control is-expanded">
|
<p class="control is-expanded">
|
||||||
<a class="button is-primary is-fullwidth">Simuler</a>
|
<a id="simulate-button" class="button is-primary is-fullwidth">Simuler</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,55 +23,6 @@
|
|||||||
& > section, & > footer {
|
& > section, & > footer {
|
||||||
font-size: $body-font-size; // we must set this on each direct child of the #simulator div, because if it's specified in em, it's multiplied by the parent font-size
|
font-size: $body-font-size; // we must set this on each direct child of the #simulator div, because if it's specified in em, it's multiplied by the parent font-size
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=number] {
|
|
||||||
-moz-appearance:textfield;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wide-label .field-label {
|
@import "app.scss";
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -33,4 +33,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
closest(elt, e => e.classList.contains('modal')).classList.toggle('is-active', false);
|
closest(elt, e => e.classList.contains('modal')).classList.toggle('is-active', false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('simulate-button').addEventListener('click', e => {
|
||||||
|
let parameters: SimulationParameters = {
|
||||||
|
batteryCapacity: Number((<HTMLInputElement>document.querySelector('[name=battery-capacity]')).value),
|
||||||
|
additionalWeight: Number((<HTMLInputElement>document.querySelector('[name=additional-weight]')).value),
|
||||||
|
climateZone: (<HTMLSelectElement>document.getElementById('zone-selector')).value,
|
||||||
|
dailyDistance: Number((<HTMLInputElement>document.querySelector('[name=daily-distance]')).value),
|
||||||
|
dailyAscendingElevation: Number((<HTMLInputElement>document.querySelector('[name=daily-elevation]')).value),
|
||||||
|
};
|
||||||
|
startSimulation(parameters);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user