Skip to content

Commit

Permalink
fix: handling charge speed for short time periods (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
enell authored Mar 10, 2023
1 parent c9086e9 commit 1810c13
Show file tree
Hide file tree
Showing 6 changed files with 431 additions and 377 deletions.
67 changes: 46 additions & 21 deletions src/fitness.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,30 @@ const calculateNormalPeriod = (g1, g2) => {
}
}

function* fillInNormalPeriodsGenerator(totalDuration, p) {
function* allPeriodsGenerator(props, excessPvEnergyUse, p) {
const { batteryMaxEnergy, soc, totalDuration } = props
let currentCharge = soc * batteryMaxEnergy

const addCosts = (period) => {
const score = calculatePeriodScore(
props,
period,
excessPvEnergyUse,
currentCharge
)
currentCharge += score[1]
period.cost = score[0]
period.charge = score[1]
return period
}

for (let i = 0; i < p.length; i += 1) {
const normalPeriod = calculateNormalPeriod(
p[i - 1] ?? { start: 0, duration: 0 },
p[i]
)
if (normalPeriod.duration > 0) yield normalPeriod
yield p[i]
if (normalPeriod.duration > 0) yield addCosts(normalPeriod)
yield addCosts(p[i])
}

const normalPeriod = calculateNormalPeriod(
Expand All @@ -44,11 +60,11 @@ function* fillInNormalPeriodsGenerator(totalDuration, p) {
start: totalDuration,
}
)
if (normalPeriod.duration > 0) yield normalPeriod
if (normalPeriod.duration > 0) yield addCosts(normalPeriod)
}

const fillInNormalPeriods = (totalDuration, p) => {
return [...fillInNormalPeriodsGenerator(totalDuration, p)]
const allPeriods = (props, excessPvEnergyUse, p) => {
return [...allPeriodsGenerator(props, excessPvEnergyUse, p)]
}

const FEED_TO_GRID = 0
Expand Down Expand Up @@ -141,18 +157,30 @@ const calculateIntervalScore = (props) => {
}
}

const calculatePeriodScore = (props, period, _currentCharge) => {
const { input, batteryMaxEnergy, batteryMaxInputPower, excessPvEnergyUse } =
props
const calculatePeriodScore = (
props,
period,
excessPvEnergyUse,
_currentCharge
) => {
const {
input,
batteryMaxEnergy,
batteryMaxInputPower,
batteryMaxOutputPower,
} = props
let cost = 0
let currentCharge = _currentCharge
for (const interval of splitIntoHourIntervals(period)) {
const duration = interval.duration / 60
const maxCharge = Math.min(
batteryMaxInputPower,
batteryMaxInputPower * duration,
batteryMaxEnergy - currentCharge
)
const maxDischarge = Math.min(batteryMaxInputPower, currentCharge)
const maxDischarge = Math.min(
batteryMaxOutputPower * duration,
currentCharge
)
const { importPrice, exportPrice, consumption, production } =
input[Math.floor(interval.start / 60)]

Expand All @@ -173,18 +201,14 @@ const calculatePeriodScore = (props, period, _currentCharge) => {
}

const fitnessFunction = (props) => (phenotype) => {
const { totalDuration, batteryMaxEnergy, soc } = props

let cost = 0
let currentCharge = soc * batteryMaxEnergy

for (const period of fillInNormalPeriodsGenerator(
totalDuration,
for (const period of allPeriodsGenerator(
props,
phenotype.excessPvEnergyUse,
phenotype.periods
)) {
const score = calculatePeriodScore(props, period, currentCharge)
cost -= score[0]
currentCharge += score[1]
cost -= period.cost
}

return cost
Expand All @@ -193,8 +217,9 @@ const fitnessFunction = (props) => (phenotype) => {
module.exports = {
fitnessFunction,
splitIntoHourIntervals,
fillInNormalPeriodsGenerator,
fillInNormalPeriods,
allPeriodsGenerator,
allPeriods,
calculatePeriodScore,
calculateDischargeScore,
calculateChargeScore,
calculateNormalScore,
Expand Down
155 changes: 77 additions & 78 deletions src/strategy-battery-charging-functions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const geneticAlgorithmConstructor = require('geneticalgorithm')
const { fitnessFunction, fillInNormalPeriodsGenerator } = require('./fitness')
const {
fitnessFunction,
allPeriodsGenerator,
calculatePeriodScore,
} = require('./fitness')

const random = (min, max) => {
return Math.floor(Math.random() * (max - min)) + min
Expand All @@ -9,16 +13,16 @@ const clamp = (num, min, max) => {
return Math.min(Math.max(num, min), max)
}

const repair = (phenotype, endTime) => {
const repair = (phenotype, totalDuration) => {
const trimGene = (gene) => {
if (gene.start < 0) {
gene.duration += Math.max(gene.start, gene.duration * -1)
gene.start = 0
}
if (gene.start > endTime) {
gene.start = endTime - 1
if (gene.start > totalDuration) {
gene.start = totalDuration - 1
}
gene.duration = clamp(gene.duration, 0, endTime - gene.start)
gene.duration = clamp(gene.duration, 0, totalDuration - gene.start)
}

const p = phenotype.sort((a, b) => a.start - b.start)
Expand All @@ -40,37 +44,40 @@ const repair = (phenotype, endTime) => {
return p
}

const mutationFunction =
(endTime, mutationRate, excessPvEnergyUse) => (phenotype) => {
const timeAdjustment = () => {
return random(0, 61) - 30
}
const mutationFunction = (props) => (phenotype) => {
const { totalDuration, mutationRate } = props

for (let i = 0; i < phenotype.periods.length; i += 1) {
const g = phenotype.periods[i]
if (Math.random() < mutationRate) {
// Mutate action
g.activity *= -1
}
if (Math.random() < mutationRate) {
// Mutate start time
const timeChange = timeAdjustment()
g.start += timeChange
g.duration -= timeChange
}
if (Math.random() < mutationRate) {
// Mutate duration
const timeChange = timeAdjustment()
g.duration += timeChange
}
const timeAdjustment = () => {
const range = totalDuration * 0.4
return random(0, range + 1) - Math.floor(range / 2)
}

for (let i = 0; i < phenotype.periods.length; i += 1) {
const g = phenotype.periods[i]
if (Math.random() < mutationRate) {
// Mutate action
g.activity *= -1
}
return {
periods: repair(phenotype.periods, endTime),
excessPvEnergyUse: excessPvEnergyUse,
if (Math.random() < mutationRate) {
// Mutate start time
const timeChange = timeAdjustment()
g.start += timeChange
g.duration -= timeChange
}
if (Math.random() < mutationRate) {
// Mutate duration
const timeChange = timeAdjustment()
g.duration += timeChange
}
}
return {
periods: repair(phenotype.periods, totalDuration),
excessPvEnergyUse: phenotype.excessPvEnergyUse,
}
}

const crossoverFunction = (endTime) => (phenotypeA, phenotypeB) => {
const crossoverFunction = (props) => (phenotypeA, phenotypeB) => {
const { totalDuration } = props
const midpoint = random(0, phenotypeA.periods.length)
const childGenes = []
for (let i = 0; i < phenotypeA.periods.length; i += 1) {
Expand All @@ -83,7 +90,7 @@ const crossoverFunction = (endTime) => (phenotypeA, phenotypeB) => {

return [
{
periods: repair(childGenes, endTime),
periods: repair(childGenes, totalDuration),
excessPvEnergyUse:
Math.random() < 0.5
? phenotypeA.excessPvEnergyUse
Expand All @@ -92,12 +99,13 @@ const crossoverFunction = (endTime) => (phenotypeA, phenotypeB) => {
]
}

const generatePopulation = (
endTime,
populationSize,
numberOfPricePeriods,
excessPvEnergyUse
) => {
const generatePopulation = (props) => {
const {
totalDuration,
populationSize,
numberOfPricePeriods,
excessPvEnergyUse,
} = props
const sortedIndex = (array, value) => {
let low = 0
let high = array.length
Expand All @@ -117,7 +125,7 @@ const generatePopulation = (
for (let j = 0; j < numberOfPricePeriods; j += 1) {
const gene = { activity: 0, start: 0, duration: 0 }
gene.activity = Math.random() < 0.5 ? -1 : 1
gene.start = random(0, endTime)
gene.start = random(0, totalDuration)
gene.duration = 0
const location = sortedIndex(timePeriods, gene)
timePeriods.splice(location, 0, gene)
Expand All @@ -127,7 +135,8 @@ const generatePopulation = (
const maxDuration = timePeriods[j + 1].start - timePeriods[j].start
timePeriods[j].duration = random(0, maxDuration)
}
const maxDuration = endTime - timePeriods[timePeriods.length - 1].start
const maxDuration =
totalDuration - timePeriods[timePeriods.length - 1].start
timePeriods[timePeriods.length - 1].duration = random(0, maxDuration)

population.push({
Expand All @@ -138,7 +147,8 @@ const generatePopulation = (
return population
}

const toSchedule = (p, start, totalDuration) => {
const toSchedule = (props, phenotype) => {
const { input } = props
const addMinutes = (date, minutes) => {
return new Date(date.getTime() + minutes * 60000)
}
Expand All @@ -155,20 +165,29 @@ const toSchedule = (p, start, totalDuration) => {
}

const schedule = []

for (const period of fillInNormalPeriodsGenerator(totalDuration, p)) {
//props, totalDuration, excessPvEnergyUse, p
const periodStart = new Date(input[0].start)
for (const period of allPeriodsGenerator(
props,
phenotype.excessPvEnergyUse,
phenotype.periods
)) {
if (period.duration <= 0) {
continue
}
let periodStart = new Date(start)

if (schedule.length && period.activity === schedule.at(-1).activity) {
schedule[schedule.length - 1].duration += period.duration
schedule.at(-1).duration += period.duration
schedule.at(-1).cost += period.cost
schedule.at(-1).charge += period.charge
} else {
schedule.push({
start: addMinutes(periodStart, period.start),
activity: period.activity,
duration: period.duration,
name: activityToName(period.activity),
duration: period.duration,
cost: period.cost,
charge: period.charge,
})
}
}
Expand Down Expand Up @@ -211,43 +230,23 @@ const mergeInput = (config) => {
}

const calculateBatteryChargingStrategy = (config) => {
const {
populationSize,
numberOfPricePeriods,
generations,
mutationRate,
batteryMaxEnergy,
batteryMaxInputPower,
soc,
excessPvEnergyUse,
} = config
const { generations } = config

const input = mergeInput(config)
if (input === undefined || input.length === 0) return {}

let totalDuration = input.length * 60

const f = fitnessFunction({
const props = {
...config,
input,
totalDuration,
batteryMaxEnergy,
batteryMaxInputPower,
soc,
})
totalDuration: input.length * 60,
}

const f = fitnessFunction(props)
const geneticAlgorithm = geneticAlgorithmConstructor({
mutationFunction: mutationFunction(
totalDuration,
mutationRate,
excessPvEnergyUse
),
crossoverFunction: crossoverFunction(totalDuration),
mutationFunction: mutationFunction(props),
crossoverFunction: crossoverFunction(props),
fitnessFunction: f,
population: generatePopulation(
totalDuration,
populationSize,
numberOfPricePeriods,
excessPvEnergyUse
),
population: generatePopulation(props),
})

for (let i = 0; i < generations; i += 1) {
Expand All @@ -258,12 +257,12 @@ const calculateBatteryChargingStrategy = (config) => {
const noBattery = { periods: [], excessPvEnergyUse: 0 }
return {
best: {
schedule: toSchedule(best.periods, input[0].start, totalDuration),
schedule: toSchedule(props, best),
excessPvEnergyUse: best.excessPvEnergyUse,
cost: f(best) * -1,
},
noBattery: {
schedule: toSchedule(noBattery.periods, input[0].start, totalDuration),
schedule: toSchedule(props, noBattery),
excessPvEnergyUse: noBattery.excessPvEnergyUse,
cost: f(noBattery) * -1,
},
Expand Down
3 changes: 1 addition & 2 deletions src/strategy-battery-charging.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const node = (RED) => {
generations,
mutationRate,
batteryMaxEnergy,
batteryMaxOutputPower,
batteryMaxInputPower,
averageConsumption,
} = config
Expand All @@ -34,7 +33,7 @@ const node = (RED) => {
generations,
mutationRate: mutationRate / 100,
batteryMaxEnergy,
batteryMaxOutputPower,
batteryMaxOutputPower: batteryMaxInputPower,
batteryMaxInputPower,
averageConsumption,
consumptionForecast,
Expand Down
Loading

1 comment on commit 1810c13

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compare with master

Benchmark suite Current: 1810c13 Previous: c9086e9 Ratio
calculate schedule 642149 ops/sec (±0.38%) 596059 ops/sec (±1.19%) 0.93

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.