From 44aec53a1f959866b4aff1bf9a3d47bad5fce1de Mon Sep 17 00:00:00 2001 From: Johan Enell Date: Fri, 5 May 2023 22:59:35 +0200 Subject: [PATCH] fix: invalid schedule (#30) --- src/fitness.js | 23 ++- src/strategy-battery-charging-functions.js | 39 ++--- test/fitness.test.js | 32 ++-- ...-battery-charging-functions-mutate.test.js | 2 +- ...trategy-battery-charging-functions.test.js | 137 +++++++++++++++++- 5 files changed, 183 insertions(+), 50 deletions(-) diff --git a/src/fitness.js b/src/fitness.js index ec6a508..1c91528 100644 --- a/src/fitness.js +++ b/src/fitness.js @@ -28,8 +28,9 @@ const calculateNormalPeriod = (g1, g2) => { } } -function* allPeriodsGenerator(props, excessPvEnergyUse, p) { +function* allPeriodsGenerator(props, phenotype) { const { batteryMaxEnergy, soc, totalDuration } = props + const { excessPvEnergyUse, periods } = phenotype let currentCharge = soc * batteryMaxEnergy const addCosts = (period) => { @@ -45,17 +46,17 @@ function* allPeriodsGenerator(props, excessPvEnergyUse, p) { return period } - for (let i = 0; i < p.length; i += 1) { + for (let i = 0; i < periods.length; i += 1) { const normalPeriod = calculateNormalPeriod( - p[i - 1] ?? { start: 0, duration: 0 }, - p[i] + periods[i - 1] ?? { start: 0, duration: 0 }, + periods[i] ) if (normalPeriod.duration > 0) yield addCosts(normalPeriod) - yield addCosts(p[i]) + yield addCosts(periods[i]) } const normalPeriod = calculateNormalPeriod( - p.at(-1) ?? { start: 0, duration: 0 }, + periods.at(-1) ?? { start: 0, duration: 0 }, { start: totalDuration, } @@ -63,8 +64,8 @@ function* allPeriodsGenerator(props, excessPvEnergyUse, p) { if (normalPeriod.duration > 0) yield addCosts(normalPeriod) } -const allPeriods = (props, excessPvEnergyUse, p) => { - return [...allPeriodsGenerator(props, excessPvEnergyUse, p)] +const allPeriods = (props, phenotype) => { + return [...allPeriodsGenerator(props, phenotype)] } const FEED_TO_GRID = 0 @@ -203,11 +204,7 @@ const calculatePeriodScore = ( const fitnessFunction = (props) => (phenotype) => { let cost = 0 - for (const period of allPeriodsGenerator( - props, - phenotype.excessPvEnergyUse, - phenotype.periods - )) { + for (const period of allPeriodsGenerator(props, phenotype)) { cost -= period.cost } diff --git a/src/strategy-battery-charging-functions.js b/src/strategy-battery-charging-functions.js index b7f199a..5ceb7d8 100644 --- a/src/strategy-battery-charging-functions.js +++ b/src/strategy-battery-charging-functions.js @@ -1,9 +1,6 @@ +const geneticalgorithm = require('geneticalgorithm') const geneticAlgorithmConstructor = require('geneticalgorithm') -const { - fitnessFunction, - allPeriodsGenerator, - calculatePeriodScore, -} = require('./fitness') +const { fitnessFunction, allPeriodsGenerator } = require('./fitness') const random = (min, max) => { return Math.floor(Math.random() * (max - min)) + min @@ -15,13 +12,7 @@ const clamp = (num, min, max) => { 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 > totalDuration) { - gene.start = totalDuration - 1 - } + gene.start = clamp(gene.start, 0, totalDuration - 1) gene.duration = clamp(gene.duration, 0, totalDuration - gene.start) } @@ -38,7 +29,7 @@ const repair = (phenotype, totalDuration) => { const adjustment = (diff / 2) * -1 g1.duration -= clamp(Math.ceil(adjustment), 0, g1.duration) g2.start += Math.floor(adjustment) - g2.duration -= clamp(Math.ceil(adjustment), 0, g2.duration) + g2.duration -= clamp(Math.floor(adjustment), 0, g2.duration) } } return p @@ -167,16 +158,12 @@ const toSchedule = (props, phenotype) => { const schedule = [] //props, totalDuration, excessPvEnergyUse, p const periodStart = new Date(input[0].start) - for (const period of allPeriodsGenerator( - props, - phenotype.excessPvEnergyUse, - phenotype.periods - )) { + for (const period of allPeriodsGenerator(props, phenotype)) { if (period.duration <= 0) { continue } - if (schedule.length && period.activity === schedule.at(-1).activity) { + if (schedule.length && period.activity == schedule.at(-1).activity) { schedule.at(-1).duration += period.duration schedule.at(-1).cost += period.cost schedule.at(-1).charge += period.charge @@ -241,13 +228,14 @@ const calculateBatteryChargingStrategy = (config) => { totalDuration: input.length * 60, } - const f = fitnessFunction(props) - const geneticAlgorithm = geneticAlgorithmConstructor({ + const options = { mutationFunction: mutationFunction(props), crossoverFunction: crossoverFunction(props), - fitnessFunction: f, + fitnessFunction: fitnessFunction(props), population: generatePopulation(props), - }) + } + + const geneticAlgorithm = geneticAlgorithmConstructor(options) for (let i = 0; i < generations; i += 1) { geneticAlgorithm.evolve() @@ -259,18 +247,19 @@ const calculateBatteryChargingStrategy = (config) => { best: { schedule: toSchedule(props, best), excessPvEnergyUse: best.excessPvEnergyUse, - cost: f(best) * -1, + cost: options.fitnessFunction(best) * -1, }, noBattery: { schedule: toSchedule(props, noBattery), excessPvEnergyUse: noBattery.excessPvEnergyUse, - cost: f(noBattery) * -1, + cost: options.fitnessFunction(noBattery) * -1, }, } } module.exports = { clamp, + repair, crossoverFunction, mutationFunction, fitnessFunction, diff --git a/test/fitness.test.js b/test/fitness.test.js index 9b4c16a..affbaf8 100644 --- a/test/fitness.test.js +++ b/test/fitness.test.js @@ -96,20 +96,26 @@ describe('Fitness - splitIntoHourIntervals', () => { describe('Fitness - allPeriods', () => { test('should test allPeriods empty', () => { - expect(allPeriods(props, 0, [])).toMatchObject([ - { start: 0, duration: 300, activity: 0 }, - ]) + expect( + allPeriods(props, { excessPvEnergyUse: 0, periods: [] }) + ).toMatchObject([{ start: 0, duration: 300, activity: 0 }]) }) test('should test allPeriods one activity', () => { expect( - allPeriods(props, 0, [{ start: 0, duration: 300, activity: 1 }]) + allPeriods(props, { + excessPvEnergyUse: 0, + periods: [{ start: 0, duration: 300, activity: 1 }], + }) ).toMatchObject([{ start: 0, duration: 300, activity: 1 }]) }) test('should test allPeriods one in the middle', () => { expect( - allPeriods(props, 0, [{ start: 120, duration: 60, activity: 1 }]) + allPeriods(props, { + excessPvEnergyUse: 0, + periods: [{ start: 120, duration: 60, activity: 1 }], + }) ).toMatchObject([ { start: 0, duration: 120, activity: 0 }, { start: 120, duration: 60, activity: 1 }, @@ -119,7 +125,10 @@ describe('Fitness - allPeriods', () => { test('should test allPeriods one long activity', () => { expect( - allPeriods(props, 0, [{ start: 100, duration: 100, activity: 1 }]) + allPeriods(props, { + excessPvEnergyUse: 0, + periods: [{ start: 100, duration: 100, activity: 1 }], + }) ).toMatchObject([ { start: 0, duration: 100, activity: 0 }, { start: 100, duration: 100, activity: 1 }, @@ -129,10 +138,13 @@ describe('Fitness - allPeriods', () => { test('should test allPeriods two activities', () => { expect( - allPeriods(props, 0, [ - { start: 70, activity: 1, duration: 80 }, - { start: 160, activity: -1, duration: 30 }, - ]) + allPeriods(props, { + excessPvEnergyUse: 0, + periods: [ + { start: 70, activity: 1, duration: 80 }, + { start: 160, activity: -1, duration: 30 }, + ], + }) ).toMatchObject([ { start: 0, duration: 70, activity: 0 }, { start: 70, duration: 80, activity: 1 }, diff --git a/test/strategy-battery-charging-functions-mutate.test.js b/test/strategy-battery-charging-functions-mutate.test.js index d5277f0..889bbfa 100644 --- a/test/strategy-battery-charging-functions-mutate.test.js +++ b/test/strategy-battery-charging-functions-mutate.test.js @@ -19,7 +19,7 @@ describe('Mutation', () => { expect(p).toMatchObject({ periods: [ - { start: 0, activity: -1, duration: 5 }, + { start: 0, activity: -1, duration: 10 }, { start: 85, activity: 1, duration: 10 }, ], excessPvEnergyUse: 0, diff --git a/test/strategy-battery-charging-functions.test.js b/test/strategy-battery-charging-functions.test.js index bdd9c7a..390cb36 100644 --- a/test/strategy-battery-charging-functions.test.js +++ b/test/strategy-battery-charging-functions.test.js @@ -1,7 +1,8 @@ -const { expect } = require('@jest/globals') +const { expect, describe, it } = require('@jest/globals') const { mockRandomForEach } = require('jest-mock-random') const { clamp, + repair, calculateBatteryChargingStrategy, crossoverFunction, } = require('../src/strategy-battery-charging-functions') @@ -163,3 +164,137 @@ describe('Calculate', () => { }, []) }) }) + +describe('Repair', () => { + it('Repair - one valid gene', () => { + const p = [{ start: 5, duration: 10 }] + expect(repair(p, 20)).toEqual(p) + }) + + it('Repair - one valid gene filling all', () => { + const p = [{ start: 0, duration: 10 }] + expect(repair(p, 10)).toEqual(p) + }) + + it('Repair - two valid genes', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 20, duration: 10 }, + ] + expect(repair(p, 50)).toEqual(p) + }) + + it('Repair - two valid genes in wrong order', () => { + const p = [ + { start: 20, duration: 10 }, + { start: 5, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 10 }, + { start: 20, duration: 10 }, + ]) + }) + + it('Repair - two genes next to each other', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 15, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 10 }, + { start: 15, duration: 10 }, + ]) + }) + + it('Repair - two genes just crossing', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 14, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 9 }, + { start: 14, duration: 10 }, + ]) + }) + + it('Repair - two genes crossing', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 10, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 7 }, + { start: 12, duration: 8 }, + ]) + }) + + it('Repair - three genes crossing', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 10, duration: 10 }, + { start: 16, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 7 }, + { start: 12, duration: 6 }, + { start: 18, duration: 8 }, + ]) + }) + + it('Repair - two genes completely overlapping', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 5, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 5 }, + { start: 10, duration: 5 }, + ]) + }) + + it('Repair - three genes completely overlapping', () => { + const p = [ + { start: 5, duration: 10 }, + { start: 5, duration: 10 }, + { start: 5, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 5, duration: 5 }, + { start: 10, duration: 0 }, + { start: 10, duration: 5 }, + ]) + }) + + it('Repair - start lower than 0', () => { + const p = [ + { start: -5, duration: 10 }, + { start: 20, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 0, duration: 10 }, + { start: 20, duration: 10 }, + ]) + }) + + it('Repair - duration higher than max', () => { + const p = [ + { start: 0, duration: 10 }, + { start: 45, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 0, duration: 10 }, + { start: 45, duration: 5 }, + ]) + }) + + it('Repair - start higher than max', () => { + const p = [ + { start: 0, duration: 10 }, + { start: 55, duration: 10 }, + ] + expect(repair(p, 50)).toEqual([ + { start: 0, duration: 10 }, + { start: 49, duration: 1 }, + ]) + }) +})