From 1810c132c0417171950a1586273a9f7c8a8ad725 Mon Sep 17 00:00:00 2001 From: Johan Enell Date: Fri, 10 Mar 2023 14:31:10 +0100 Subject: [PATCH] fix: handling charge speed for short time periods (#14) --- src/fitness.js | 67 +- src/strategy-battery-charging-functions.js | 155 +++-- src/strategy-battery-charging.js | 3 +- test/fitness.test.js | 573 +++++++++--------- ...-battery-charging-functions-mutate.test.js | 6 +- ...trategy-battery-charging-functions.test.js | 4 +- 6 files changed, 431 insertions(+), 377 deletions(-) diff --git a/src/fitness.js b/src/fitness.js index e6c7d11..ec6a508 100644 --- a/src/fitness.js +++ b/src/fitness.js @@ -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( @@ -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 @@ -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)] @@ -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 @@ -193,8 +217,9 @@ const fitnessFunction = (props) => (phenotype) => { module.exports = { fitnessFunction, splitIntoHourIntervals, - fillInNormalPeriodsGenerator, - fillInNormalPeriods, + allPeriodsGenerator, + allPeriods, + calculatePeriodScore, calculateDischargeScore, calculateChargeScore, calculateNormalScore, diff --git a/src/strategy-battery-charging-functions.js b/src/strategy-battery-charging-functions.js index 8b6d0fc..b7f199a 100644 --- a/src/strategy-battery-charging-functions.js +++ b/src/strategy-battery-charging-functions.js @@ -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 @@ -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) @@ -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) { @@ -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 @@ -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 @@ -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) @@ -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({ @@ -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) } @@ -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, }) } } @@ -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) { @@ -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, }, diff --git a/src/strategy-battery-charging.js b/src/strategy-battery-charging.js index 6ede374..1980824 100644 --- a/src/strategy-battery-charging.js +++ b/src/strategy-battery-charging.js @@ -14,7 +14,6 @@ const node = (RED) => { generations, mutationRate, batteryMaxEnergy, - batteryMaxOutputPower, batteryMaxInputPower, averageConsumption, } = config @@ -34,7 +33,7 @@ const node = (RED) => { generations, mutationRate: mutationRate / 100, batteryMaxEnergy, - batteryMaxOutputPower, + batteryMaxOutputPower: batteryMaxInputPower, batteryMaxInputPower, averageConsumption, consumptionForecast, diff --git a/test/fitness.test.js b/test/fitness.test.js index f65c5d1..9b4c16a 100644 --- a/test/fitness.test.js +++ b/test/fitness.test.js @@ -1,13 +1,66 @@ -const { expect } = require('@jest/globals') +const { expect, describe } = require('@jest/globals') const { fitnessFunction, splitIntoHourIntervals, - fillInNormalPeriods, + allPeriods, + calculatePeriodScore, calculateDischargeScore, calculateChargeScore, calculateNormalScore, } = require('../src/fitness') +let props + +beforeEach(() => { + let now = Date.now() + now = now - (now % (60 * 60 * 1000)) + const input = [ + { + start: new Date(now).toString(), + importPrice: 1, + exportPrice: 1, + consumption: 1, + production: 0, + }, + { + start: new Date(now + 60 * 60 * 1000).toString(), + importPrice: 1, + exportPrice: 1, + consumption: 1, + production: 0, + }, + { + start: new Date(now + 60 * 60 * 1000 * 2).toString(), + importPrice: 1, + exportPrice: 1, + consumption: 1, + production: 0, + }, + { + start: new Date(now + 60 * 60 * 1000 * 3).toString(), + importPrice: 1, + exportPrice: 1, + consumption: 1, + production: 0, + }, + { + start: new Date(now + 60 * 60 * 1000 * 4).toString(), + importPrice: 1, + exportPrice: 1, + consumption: 1, + production: 0, + }, + ] + props = { + input, + totalDuration: input.length * 60, + batteryMaxEnergy: 1, + batteryMaxInputPower: 1, + batteryMaxOutputPower: 1, + soc: 1, + } +}) + describe('Fitness - splitIntoHourIntervals', () => { test('should split into one intervals', () => { expect( @@ -41,22 +94,22 @@ describe('Fitness - splitIntoHourIntervals', () => { }) }) -describe('Fitness - fillInNormalPeriods', () => { - test('should test fillInNormalPeriods empty', () => { - expect(fillInNormalPeriods(300, [])).toMatchObject([ +describe('Fitness - allPeriods', () => { + test('should test allPeriods empty', () => { + expect(allPeriods(props, 0, [])).toMatchObject([ { start: 0, duration: 300, activity: 0 }, ]) }) - test('should test fillInNormalPeriods one activity', () => { + test('should test allPeriods one activity', () => { expect( - fillInNormalPeriods(300, [{ start: 0, duration: 300, activity: 1 }]) + allPeriods(props, 0, [{ start: 0, duration: 300, activity: 1 }]) ).toMatchObject([{ start: 0, duration: 300, activity: 1 }]) }) - test('should test fillInNormalPeriods one in the middle', () => { + test('should test allPeriods one in the middle', () => { expect( - fillInNormalPeriods(300, [{ start: 120, duration: 60, activity: 1 }]) + allPeriods(props, 0, [{ start: 120, duration: 60, activity: 1 }]) ).toMatchObject([ { start: 0, duration: 120, activity: 0 }, { start: 120, duration: 60, activity: 1 }, @@ -64,9 +117,9 @@ describe('Fitness - fillInNormalPeriods', () => { ]) }) - test('should test fillInNormalPeriods one long activity', () => { + test('should test allPeriods one long activity', () => { expect( - fillInNormalPeriods(300, [{ start: 100, duration: 100, activity: 1 }]) + allPeriods(props, 0, [{ start: 100, duration: 100, activity: 1 }]) ).toMatchObject([ { start: 0, duration: 100, activity: 0 }, { start: 100, duration: 100, activity: 1 }, @@ -74,9 +127,9 @@ describe('Fitness - fillInNormalPeriods', () => { ]) }) - test('should test fillInNormalPeriods two activities', () => { + test('should test allPeriods two activities', () => { expect( - fillInNormalPeriods(300, [ + allPeriods(props, 0, [ { start: 70, activity: 1, duration: 80 }, { start: 160, activity: -1, duration: 30 }, ]) @@ -90,281 +143,267 @@ describe('Fitness - fillInNormalPeriods', () => { }) }) -describe('Fitness - calculateDischargeScore', () => { - test('should discharge full hour, full battery', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxDischarge: 1, - }) - ).toEqual([0, -1]) - }) +describe('Fitness - calculateScore', () => { + describe('Fitness - calculateDischargeScore', () => { + test('should discharge full hour, full battery', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxDischarge: 1, + }) + ).toEqual([0, -1]) + }) - test('should discharge full hour, empty battery', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxDischarge: 0, - }) - ).toEqual([2, 0]) - }) + test('should discharge full hour, empty battery', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxDischarge: 0, + }) + ).toEqual([2, 0]) + }) - test('should discharge full hour, almost empty battery', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxDischarge: 0.5, - }) - ).toEqual([1, -0.5]) - }) + test('should discharge full hour, almost empty battery', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxDischarge: 0.5, + }) + ).toEqual([1, -0.5]) + }) - test('should discharge full hour, full battery, equal production', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 1, - maxDischarge: 1, - }) - ).toEqual([0, 0]) - }) + test('should discharge full hour, full battery, equal production', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 1, + maxDischarge: 1, + }) + ).toEqual([0, 0]) + }) - test('should discharge full hour, full battery, double production', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 2, - maxDischarge: 1, - }) - ).toEqual([-2, 0]) - }) + test('should discharge full hour, full battery, double production', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 2, + maxDischarge: 1, + }) + ).toEqual([-2, 0]) + }) - test('should discharge full hour, full battery, double production, charge preference', () => { - expect( - calculateDischargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 2, - maxDischarge: 1, - maxCharge: 1, - excessPvEnergyUse: 1, - }) - ).toEqual([0, 1]) + test('should discharge full hour, full battery, double production, charge preference', () => { + expect( + calculateDischargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 2, + maxDischarge: 1, + maxCharge: 1, + excessPvEnergyUse: 1, + }) + ).toEqual([0, 1]) + }) }) -}) -describe('Fitness - calculateChargeScore', () => { - test('should charge full hour, full battery', () => { - expect( - calculateChargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxCharge: 0, - }) - ).toEqual([2, 0]) - }) + describe('Fitness - calculateChargeScore', () => { + test('should charge full hour, full battery', () => { + expect( + calculateChargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxCharge: 0, + }) + ).toEqual([2, 0]) + }) - test('should charge full hour, empty battery', () => { - expect( - calculateChargeScore({ - duration: 1, - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxCharge: 1, - }) - ).toEqual([4, 1]) - }) + test('should charge full hour, empty battery', () => { + expect( + calculateChargeScore({ + duration: 1, + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxCharge: 1, + }) + ).toEqual([4, 1]) + }) - test('should charge full hour, almost full battery', () => { - expect( - calculateChargeScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxCharge: 0.5, - }) - ).toEqual([3, 0.5]) - }) + test('should charge full hour, almost full battery', () => { + expect( + calculateChargeScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxCharge: 0.5, + }) + ).toEqual([3, 0.5]) + }) - test('should charge full hour, empty battery, equal production', () => { - expect( - calculateChargeScore({ - duration: 1, - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 1, - maxCharge: 1, - }) - ).toEqual([2, 1]) - }) + test('should charge full hour, empty battery, equal production', () => { + expect( + calculateChargeScore({ + duration: 1, + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 1, + maxCharge: 1, + }) + ).toEqual([2, 1]) + }) - test('should charge full hour, empty battery, double production', () => { - expect( - calculateChargeScore({ - duration: 1, - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 2, - maxCharge: 1, - }) - ).toEqual([0, 1]) - }) + test('should charge full hour, empty battery, double production', () => { + expect( + calculateChargeScore({ + duration: 1, + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 2, + maxCharge: 1, + }) + ).toEqual([0, 1]) + }) - test('should charge full hour, empty battery, triple production, charge preference', () => { - expect( - calculateChargeScore({ - duration: 1, - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 3, - maxCharge: 1, - excessPvEnergyUse: 1, - }) - ).toEqual([-2, 1]) + test('should charge full hour, empty battery, triple production, charge preference', () => { + expect( + calculateChargeScore({ + duration: 1, + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 3, + maxCharge: 1, + excessPvEnergyUse: 1, + }) + ).toEqual([-2, 1]) + }) }) -}) -describe('Fitness - calculateNormalScore', () => { - test('should consume normal full hour no production', () => { - expect( - calculateNormalScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 0, - maxCharge: 1, - }) - ).toEqual([2, 0]) - }) + describe('Fitness - calculateNormalScore', () => { + test('should consume normal full hour no production', () => { + expect( + calculateNormalScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 0, + maxCharge: 1, + }) + ).toEqual([2, 0]) + }) - test('should consume normal full hour with equal production', () => { - expect( - calculateNormalScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 1, - maxCharge: 1, - }) - ).toEqual([0, 0]) - }) + test('should consume normal full hour with equal production', () => { + expect( + calculateNormalScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 1, + maxCharge: 1, + }) + ).toEqual([0, 0]) + }) - test('should consume normal full hour with double production, charge preference', () => { - expect( - calculateNormalScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 2, - maxCharge: 1, - excessPvEnergyUse: 1, - }) - ).toEqual([0, 1]) + test('should consume normal full hour with double production, charge preference', () => { + expect( + calculateNormalScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 2, + maxCharge: 1, + excessPvEnergyUse: 1, + }) + ).toEqual([0, 1]) + }) + + test('should consume normal full hour with double production, feed to grid preference', () => { + expect( + calculateNormalScore({ + importPrice: 2, + exportPrice: 2, + consumption: 1, + production: 2, + maxCharge: 1, + excessPvEnergyUse: 0, + }) + ).toEqual([-2, 0]) + }) }) - test('should consume normal full hour with double production, feed to grid preference', () => { - expect( - calculateNormalScore({ - importPrice: 2, - exportPrice: 2, - consumption: 1, - production: 2, - maxCharge: 1, - excessPvEnergyUse: 0, - }) - ).toEqual([-2, 0]) + describe('Fitness - calculatePeriodScore', () => { + test('shod not charge faster than max input power', () => { + const period = { start: 0, duration: 1, activity: 1 } + const currentCharge = 0 + const excessPvEnergyUse = 0 + const score = calculatePeriodScore( + props, + period, + excessPvEnergyUse, + currentCharge + ) + const chargeSpeed = score[1] / (1 / 60) + expect(chargeSpeed).toBeCloseTo(props.batteryMaxInputPower) + expect(score[0]).toBeCloseTo(2 / 60) + expect(score[1]).toBeCloseTo(1 / 60) + }) + + test('shod not discharge faster than max output power', () => { + const period = { start: 0, duration: 1, activity: -1 } + const currentCharge = 100 + const excessPvEnergyUse = 0 + const score = calculatePeriodScore( + props, + period, + excessPvEnergyUse, + currentCharge + ) + const dischargeSpeed = (score[1] / (1 / 60)) * -1 + expect(dischargeSpeed).toBeCloseTo(props.batteryMaxOutputPower) + expect(score[0]).toBeCloseTo(0) + expect(score[1]).toBeCloseTo((1 / 60) * -1) + }) }) }) describe('Fitness', () => { test('should calculate fitness', () => { - const input = [ - { - start: '2022-12-01T00:00:00.000Z', - importPrice: 1, - exportPrice: 1, - consumption: 1, - production: 0, - }, - { - start: '2022-12-01T01:00:00.000Z', - importPrice: 1, - exportPrice: 1, - consumption: 1, - production: 0, - }, - ] - const totalDuration = 2 * 60 - const batteryMaxEnergy = 1 - const batteryMaxInputPower = 1 - const soc = 0 - const score = fitnessFunction({ - totalDuration, - input, - batteryMaxEnergy, - batteryMaxInputPower, - soc, - })({ + props.totalDuration = 180 + props.soc = 0 + const score = fitnessFunction(props)({ periods: [ { start: 30, duration: 60, activity: 1 }, { start: 90, duration: 30, activity: -1 }, ], excessPvEnergyUse: 0, }) - expect(score).toEqual(-2.5) + expect(score).toEqual(-3.5) }) test('should calculate fitness with soc', () => { - const input = [ - { - start: '2022-12-01T00:00:00.000Z', - importPrice: 1, - exportPrice: 1, - consumption: 1, - production: 0, - }, - { - start: '2022-12-01T01:00:00.000Z', - importPrice: 1, - exportPrice: 1, - consumption: 1, - production: 0, - }, - ] - const totalDuration = 2 * 60 - const batteryMaxEnergy = 1 - const batteryMaxInputPower = 1 - const averageConsumption = 1 - const averageProduction = 0 - const soc = 1 - const score = fitnessFunction({ - input, - totalDuration, - batteryMaxEnergy, - batteryMaxInputPower, - soc, - })({ + props.totalDuration = 120 + props.soc = 1 + const score = fitnessFunction(props)({ periods: [ { start: 30, duration: 60, activity: 1 }, { start: 90, duration: 30, activity: -1 }, @@ -374,10 +413,12 @@ describe('Fitness', () => { expect(score).toEqual(-1.5) }) - test('should calculate 180 min charge period', () => { + test('should calculate 180 min charge period with full battery', () => { + props.totalDuration = 180 + props.soc = 1 let now = Date.now() now = now - (now % (60 * 60 * 1000)) - const input = [ + props.input = [ { start: new Date(now).toString(), importPrice: 1, @@ -400,22 +441,10 @@ describe('Fitness', () => { production: 0, }, ] - const totalDuration = 3 * 60 - const batteryMaxEnergy = 3 // kWh - const batteryMaxInputPower = 3 // kW - const soc = 0 - let score = fitnessFunction({ - input, - totalDuration, - batteryMaxEnergy, - batteryMaxInputPower, - soc, - })({ - periods: [{ start: 0, duration: 180, activity: -1 }], + let score = fitnessFunction(props)({ + periods: [{ start: 0, duration: 180, activity: 1 }], excessPvEnergyUse: 0, }) expect(score).toEqual(-1501.5) - - console.log(score) }) }) diff --git a/test/strategy-battery-charging-functions-mutate.test.js b/test/strategy-battery-charging-functions-mutate.test.js index c77fe68..d5277f0 100644 --- a/test/strategy-battery-charging-functions-mutate.test.js +++ b/test/strategy-battery-charging-functions-mutate.test.js @@ -7,7 +7,7 @@ describe('Mutation', () => { mockRandomForEach(0.4) test('should mutate', () => { - const mutate = mutationFunction(120, 1, 0) + const mutate = mutationFunction({ totalDuration: 120, mutationRate: 1 }) const p = mutate({ periods: [ @@ -19,8 +19,8 @@ describe('Mutation', () => { expect(p).toMatchObject({ periods: [ - { start: 0, activity: -1, duration: 4 }, - { start: 84, activity: 1, duration: 10 }, + { start: 0, activity: -1, duration: 5 }, + { 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 0cec830..bdd9c7a 100644 --- a/test/strategy-battery-charging-functions.test.js +++ b/test/strategy-battery-charging-functions.test.js @@ -20,7 +20,7 @@ describe('Crossover', () => { mockRandomForEach(0.4) test('should perform a crossover', () => { - const crossover = crossoverFunction(120) + const crossover = crossoverFunction({ totalDuration: 120 }) const p = crossover( { @@ -117,6 +117,8 @@ describe('Calculate', () => { activity: 0, }) expect(strategy.best.excessPvEnergyUse).toEqual(excessPvEnergyUse) + expect(strategy.best.cost).not.toBeNull() + expect(strategy.best.cost).not.toBeNaN() console.log(`best: ${strategy.best.cost}`) console.log(`no battery: ${strategy.noBattery.cost}`)