diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa96f64..8dea0c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ETS forecasts. A median fallback is used when the available history is too short for ETS. ### Changed +- Replace the fixed DEAP variation loop with adaptive genetic evolution. Crossover offspring may + now also mutate; population diversity and stagnation are tracked per generation; diversity + boosts inject fresh educated/random candidates; and incumbent-preserving soft restarts recover + from collapsed populations without stopping the run early. +- Apply small point mutations only to future, fitness-relevant controls and choose point, coherent + block, energy-shift, or flexible-device mutations as alternatives instead of stacking random + changes on top of every specialized move. Tournament selection retains useful duplicates while + enforcing a 30% minimum diversity floor. - Scale the diverse genetic start population with the configured population size while preserving the established 300-member mix: exact warm starts, locally mutated neighbours, randomized domain-informed battery/direct-marketing/EV/appliance schedules, and a guaranteed random @@ -85,6 +93,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). are deprecated in favour of `appliance_starts` and `result.home_appliance_energy_wh`. ### Fixed +- Exclude elapsed control slots from fitness-cache keys and clear cached genome tuples after run + metrics are captured, avoiding false misses and delayed memory retention in long-lived API + processes. Random EV individuals now also keep the fixed horizon tail switched off. - Respect `optimization.genetic.individuals` and `optimization.genetic.generations` independently in automatic and `/optimize` runs. Previously the individual count was accidentally passed as the generation count, the configured generation count was ignored, and every generation still diff --git a/src/akkudoktoreos/optimization/genetic/genetic.py b/src/akkudoktoreos/optimization/genetic/genetic.py index 19e23078..85a442dd 100644 --- a/src/akkudoktoreos/optimization/genetic/genetic.py +++ b/src/akkudoktoreos/optimization/genetic/genetic.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from typing import Any, Optional import numpy as np -from deap import algorithms, base, creator, tools +from deap import base, creator, tools from loguru import logger from numpydantic import NDArray, Shape from pydantic import ConfigDict, Field @@ -566,11 +566,20 @@ class GeneticOptimization(OptimizationBase): WARM_START_COPY_FRACTION = 0.10 WARM_START_MUTATION_FRACTION = 0.20 EDUCATED_GUESS_FRACTION = 0.40 - BLOCK_MUTATION_PROBABILITY = 0.20 - ENERGY_SHIFT_MUTATION_PROBABILITY = 0.35 LOCAL_SEARCH_MAX_EVALUATIONS = 96 LOCAL_SEARCH_MAX_PASSES = 4 EDUCATED_GUESS_EXPORT_QUANTILES = (0.60, 0.75, 0.90) + CROSSOVER_PROBABILITY = 0.50 + MUTATION_PROBABILITY = 0.55 + STAGNATION_MUTATION_PROBABILITY = 0.80 + STAGNATION_GENERATIONS = 8 + SOFT_RESTART_GENERATIONS = 20 + DIVERSITY_BOOST_THRESHOLD = 0.35 + SELECTION_DIVERSITY_FLOOR = 0.30 + SOFT_RESTART_DIVERSITY_THRESHOLD = 0.10 + IMMIGRANT_FRACTION = 0.12 + SOFT_RESTART_SURVIVOR_FRACTION = 0.20 + POINT_MUTATION_EXPECTED_GENES = 3.0 # Slot-math helpers — single source of truth for the optimization grid. # At the default optimization interval of 3600 s, slot_duration_h is 1.0 and @@ -1146,46 +1155,104 @@ class GeneticOptimization(OptimizationBase): individual[target_slot] = self_state if pv[target_slot] > 0.0 else len_bat return True - def mutate(self, individual: list[int]) -> tuple[list[int]]: - """Custom mutation function for the individual.""" + @staticmethod + def _force_segment_change(values: list[int], low: int, up: int) -> bool: + """Change one value when probabilistic mutation produced no effective change.""" + if not values or up <= low: + return False + position = random.randrange(len(values)) # noqa: S311 + old_value = int(values[position]) + replacement = random.randint(low, up - 1) # noqa: S311 + if replacement >= old_value: + replacement += 1 + values[position] = replacement + return True + + def _mutate_point_controls(self, individual: list[int]) -> bool: + """Apply a small point mutation only to controls that can still affect fitness.""" + changed = False + start_slot = self._start_day_slot() total_states = self._battery_state_layout().total_states + battery_part = list(individual[start_slot : self.total_slots]) + battery_before = list(battery_part) + (battery_part,) = self.toolbox.mutate_charge_discharge(battery_part) + if battery_part == battery_before: + self._force_segment_change(battery_part, 0, total_states - 1) + if battery_part != battery_before: + individual[start_slot : self.total_slots] = battery_part + changed = True - # 1. Mutating the charge_discharge part - charge_discharge_part = individual[: self.total_slots] - (charge_discharge_mutated,) = self.toolbox.mutate_charge_discharge(charge_discharge_part) + if self.optimize_ev and random.random() < 0.40: # noqa: S311 + ev_start = self.total_slots + start_slot + ev_end = self.total_slots * 2 - self.fixed_eauto_hours + ev_part = list(individual[ev_start:ev_end]) + ev_before = list(ev_part) + (ev_part,) = self.toolbox.mutate_ev_charge_index(ev_part) + if ev_part == ev_before: + self._force_segment_change(ev_part, 0, len(self.ev_possible_charge_values) - 1) + if ev_part != ev_before: + individual[ev_start:ev_end] = ev_part + changed = True - # Instead of a fixed clamping to 0..8 or 0..6 dynamically: - charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, total_states - 1) - individual[: self.total_slots] = charge_discharge_mutated + return changed - # Point mutation alone struggles with energy-coupled valleys: removing - # an export is temporarily worse until several later bypass slots also - # consume the retained energy. Add coherent neighbourhood moves that - # can cross that valley in one offspring. - if random.random() < self.BLOCK_MUTATION_PROBABILITY: # noqa: S311 - self._mutate_battery_block(individual) - if random.random() < self.ENERGY_SHIFT_MUTATION_PROBABILITY: # noqa: S311 - self._mutate_energy_shift(individual) - - # 2. Mutating the EV charge part, if active + def _mutate_flexible_controls(self, individual: list[int]) -> bool: + """Mutate EV or appliance controls without disturbing a good battery schedule.""" + changed = False if self.optimize_ev: - ev_charge_part = individual[self.total_slots : self.total_slots * 2] - (ev_charge_part_mutated,) = self.toolbox.mutate_ev_charge_index(ev_charge_part) - ev_charge_part_mutated[self.total_slots - self.fixed_eauto_hours :] = [ - 0 - ] * self.fixed_eauto_hours - individual[self.total_slots : self.total_slots * 2] = ev_charge_part_mutated + ev_start = self.total_slots + self._start_day_slot() + ev_end = self.total_slots * 2 - self.fixed_eauto_hours + ev_part = list(individual[ev_start:ev_end]) + ev_before = list(ev_part) + (ev_part,) = self.toolbox.mutate_ev_charge_index(ev_part) + if ev_part == ev_before: + self._force_segment_change(ev_part, 0, len(self.ev_possible_charge_values) - 1) + if ev_part != ev_before: + individual[ev_start:ev_end] = ev_part + changed = True - # 3. Mutating the appliance start genes. Each gene is an index into its - # own allowed_start_slots list, so the redraw stays within valid range. n_appliance_genes = self.appliance_layout.n_genes if n_appliance_genes > 0: base = len(individual) - n_appliance_genes - appliance_mutation_probability = 0.2 - for position, gene in enumerate(self.appliance_layout.genes): - if random.random() < appliance_mutation_probability: # noqa: S311 - upper = len(gene.allowed_start_slots) - 1 - individual[base + position] = random.randint(0, upper) # noqa: S311 + mutable_positions = [ + (base + position, len(gene.allowed_start_slots) - 1) + for position, gene in enumerate(self.appliance_layout.genes) + if len(gene.allowed_start_slots) > 1 + ] + if mutable_positions: + position, upper = random.choice(mutable_positions) # noqa: S311 + old_value = int(individual[position]) + replacement = random.randint(0, upper - 1) # noqa: S311 + if replacement >= old_value: + replacement += 1 + individual[position] = replacement + changed = True + return changed + + def mutate(self, individual: list[int]) -> tuple[list[int]]: + """Apply one coherent mutation family instead of stacking destructive changes.""" + operation = random.random() # noqa: S311 + changed = False + if operation < 0.50: + changed = self._mutate_point_controls(individual) + elif operation < 0.70: + before = list(individual) + self._mutate_battery_block(individual) + changed = individual != before + elif operation < 0.90: + changed = self._mutate_energy_shift(individual) + else: + changed = self._mutate_flexible_controls(individual) + + # Some specialized moves are unavailable without EV, appliances or a + # viable grid-export opportunity. Always return a genuinely changed + # future control so an offspring budget is not silently wasted. + if not changed: + self._mutate_point_controls(individual) + + if self.optimize_ev and self.fixed_eauto_hours > 0: + ev_end = self.total_slots * 2 + individual[ev_end - self.fixed_eauto_hours : ev_end] = [0] * self.fixed_eauto_hours return (individual,) @@ -1198,9 +1265,10 @@ class GeneticOptimization(OptimizationBase): # Add EV charge index values if optimize_ev is True if self.optimize_ev: - individual_components += [ - self.toolbox.attr_ev_charge_index() for _ in range(self.total_slots) - ] + ev_controls = [self.toolbox.attr_ev_charge_index() for _ in range(self.total_slots)] + if self.fixed_eauto_hours > 0: + ev_controls[-self.fixed_eauto_hours :] = [0] * self.fixed_eauto_hours + individual_components += ev_controls # Add one appliance start gene per scheduled run (index into that run's # allowed_start_slots). No draws happen when there are no appliances, so @@ -1714,6 +1782,269 @@ class GeneticOptimization(OptimizationBase): final_value = float(best.fitness.values[0]) return best, evaluations, improvements, initial_value, final_value + def _population_diversity(self, population: list[Any]) -> float: + """Return the fraction of fitness-relevant unique genomes.""" + if not population: + return 0.0 + return len({self._fitness_key(individual) for individual in population}) / len(population) + + def _invalidate_individual(self, individual: Any) -> None: + """Invalidate inherited fitness and auxiliary simulation values.""" + if individual.fitness.valid: + del individual.fitness.values + if hasattr(individual, "extra_data"): + del individual.extra_data + + def _evaluate_invalid(self, population: list[Any]) -> int: + """Evaluate invalid individuals and return the number of cache lookups.""" + invalid = [individual for individual in population if not individual.fitness.valid] + fitnesses = self.toolbox.map(self.toolbox.evaluate, invalid) + for individual, fitness in zip(invalid, fitnesses): + individual.fitness.values = fitness + return len(invalid) + + def _fresh_population(self, count: int, *, educated_fraction: float) -> list[Any]: + """Create a mixed set of current educated guesses and random immigrants.""" + if count <= 0: + return [] + educated_target = min(count, int(count * educated_fraction + 0.5)) + educated = self._educated_guess_individuals(educated_target) + fresh = [creator.Individual(genome) for genome in educated[:count]] + fresh.extend(self.toolbox.population(n=count - len(fresh))) + return fresh + + def _best_unique(self, population: list[Any], count: int) -> list[Any]: + """Return the best fitness-relevant unique candidates.""" + selected: list[Any] = [] + seen: set[tuple[int, ...]] = set() + for candidate in tools.selBest(population, len(population)): + key = self._fitness_key(candidate) + if key in seen: + continue + seen.add(key) + selected.append(candidate) + if len(selected) >= count: + break + return selected + + def _select_diverse(self, candidates: list[Any], count: int) -> list[Any]: + """Tournament-select while repairing only severe duplicate takeover.""" + if not candidates or count <= 0: + return [] + + selected = tools.selTournament(candidates, count, tournsize=3) + best = tools.selBest(candidates, 1)[0] + best_key = self._fitness_key(best) + selected_keys = [self._fitness_key(candidate) for candidate in selected] + if best_key not in selected_keys: + worst_index = max( + range(len(selected)), + key=lambda index: selected[index].fitness.values[0], + ) + selected[worst_index] = best + selected_keys[worst_index] = best_key + + # Duplicates are useful for exploitation and cache hits. Replace only + # enough duplicate selections to keep a minimum search breadth. + target_unique = min( + count, + max(1, int(count * self.SELECTION_DIVERSITY_FLOOR + 0.999999)), + ) + key_counts: dict[tuple[int, ...], int] = defaultdict(int) + for key in selected_keys: + key_counts[key] += 1 + if len(key_counts) >= target_unique: + return selected + + for candidate in tools.selBest(candidates, len(candidates)): + candidate_key = self._fitness_key(candidate) + if candidate_key in key_counts: + continue + replaceable = [index for index, key in enumerate(selected_keys) if key_counts[key] > 1] + if not replaceable: + break + replace_index = max( + replaceable, + key=lambda index: selected[index].fitness.values[0], + ) + replaced_key = selected_keys[replace_index] + key_counts[replaced_key] -= 1 + selected[replace_index] = candidate + selected_keys[replace_index] = candidate_key + key_counts[candidate_key] = 1 + if len(key_counts) >= target_unique: + break + return selected + + def _make_offspring( + self, + population: list[Any], + count: int, + *, + mutation_probability: float, + ) -> list[Any]: + """Create offspring where crossover and mutation can both be applied.""" + offspring: list[Any] = [] + for _ in range(count): + child = self.toolbox.clone(random.choice(population)) # noqa: S311 + crossed = False + if len(population) > 1 and random.random() < self.CROSSOVER_PROBABILITY: # noqa: S311 + partner = self.toolbox.clone(random.choice(population)) # noqa: S311 + child, _ = self.toolbox.mate(child, partner) + crossed = True + + # Non-crossover offspring are always mutated. Crossover children are + # independently mutated, preventing identical parents from turning + # most of the generation into unchanged copies. + if not crossed or random.random() < mutation_probability: # noqa: S311 + (child,) = self.toolbox.mutate(child) + self._invalidate_individual(child) + offspring.append(child) + return offspring + + def _evolve_population_adaptive( + self, + population: list[Any], + *, + mu: int, + lambda_: int, + ngen: int, + stats: Any, + halloffame: Any, + ) -> tuple[list[Any], Any]: + """Evolve with diversity boosts and incumbent-preserving soft restarts.""" + logbook = tools.Logbook() + logbook.header = [ + "gen", + "nevals", + *stats.fields, + "diversity", + "stagnation", + "immigrants", + "restart", + ] + + nevals = self._evaluate_invalid(population) + halloffame.update(population) + best_fitness = float(halloffame[0].fitness.values[0]) + stagnation = 0 + diversity = self._population_diversity(population) + record = stats.compile(population) + logbook.record( + gen=0, + nevals=nevals, + diversity=diversity, + stagnation=stagnation, + immigrants=0, + restart=0, + **record, + ) + if self.verbose: + print(logbook.stream) + + diversity_boost_active = False + soft_restarts = 0 + total_immigrants = 0 + minimum_diversity = diversity + for generation in range(1, ngen + 1): + diversity = self._population_diversity(population) + soft_restart = ( + stagnation >= self.SOFT_RESTART_GENERATIONS + or diversity < self.SOFT_RESTART_DIVERSITY_THRESHOLD + ) + immigrants = 0 + + if soft_restart: + survivor_count = max(1, int(mu * self.SOFT_RESTART_SURVIVOR_FRACTION)) + survivors = self._best_unique(population, survivor_count) + immigrants = mu - len(survivors) + population = survivors + self._fresh_population( + immigrants, + educated_fraction=0.40, + ) + nevals = self._evaluate_invalid(population) + halloffame.update(population) + soft_restarts += 1 + total_immigrants += immigrants + stagnation = 0 + diversity_boost_active = False + logger.info( + "Genetic soft restart at generation {}: kept {} unique survivors, " + "injected {} immigrants (diversity {:.1%}).", + generation, + len(survivors), + immigrants, + diversity, + ) + else: + diversity_boost = ( + stagnation >= self.STAGNATION_GENERATIONS + or diversity < self.DIVERSITY_BOOST_THRESHOLD + ) + if diversity_boost and not diversity_boost_active: + logger.info( + "Genetic diversity boost at generation {}: stagnation {}, " + "diversity {:.1%}.", + generation, + stagnation, + diversity, + ) + diversity_boost_active = diversity_boost + mutation_probability = ( + self.STAGNATION_MUTATION_PROBABILITY + if diversity_boost + else self.MUTATION_PROBABILITY + ) + if diversity_boost: + immigrants = max(1, int(lambda_ * self.IMMIGRANT_FRACTION + 0.5)) + offspring = self._make_offspring( + population, + lambda_ - immigrants, + mutation_probability=mutation_probability, + ) + offspring.extend( + self._fresh_population( + immigrants, + educated_fraction=0.50, + ) + ) + nevals = self._evaluate_invalid(offspring) + halloffame.update(offspring) + population = self._select_diverse(population + offspring, mu) + total_immigrants += immigrants + + current_best = float(halloffame[0].fitness.values[0]) + if current_best < best_fitness - 1e-9: + best_fitness = current_best + stagnation = 0 + diversity_boost_active = False + elif not soft_restart: + stagnation += 1 + + diversity = self._population_diversity(population) + minimum_diversity = min(minimum_diversity, diversity) + record = stats.compile(population) + logbook.record( + gen=generation, + nevals=nevals, + diversity=diversity, + stagnation=stagnation, + immigrants=immigrants, + restart=int(soft_restart), + **record, + ) + if self.verbose: + print(logbook.stream) + + self._adaptive_evolution_metrics = { + "soft_restarts": soft_restarts, + "immigrants": total_immigrants, + "minimum_diversity": minimum_diversity, + "final_diversity": self._population_diversity(population), + "final_stagnation": stagnation, + } + return population, logbook + def setup_deap_environment(self, opti_param: dict[str, Any], start_hour: int) -> None: """Set up the DEAP environment with fitness and individual creation rules.""" self.opti_param = opti_param @@ -1756,10 +2087,15 @@ class GeneticOptimization(OptimizationBase): self.toolbox.register("population", tools.initRepeat, list, self.toolbox.individual) self.toolbox.register("mate", tools.cxTwoPoint) - # Mutation operator for battery charge/discharge states - # Keep the expected number of mutated genes per hour stable when the - # interval becomes finer (0.2 hourly -> 0.05 on a quarter-hour grid). - mutation_probability = 0.2 / self.slots_per_hour + # Keep point mutations local enough to refine a mature schedule. The + # expected number of changed controls remains close to three regardless + # of interval and elapsed slots; coherent block/energy moves are handled + # by separate mutation families. + active_slots = max(self.total_slots - self._start_day_slot(), 1) + mutation_probability = min( + 0.10, + self.POINT_MUTATION_EXPECTED_GENES / active_slots, + ) self.toolbox.register( "mutate_charge_discharge", tools.mutUniformInt, @@ -1838,7 +2174,7 @@ class GeneticOptimization(OptimizationBase): if not getattr(self, "_fitness_cache_enabled", False): return self._evaluate_uncached(individual, parameters, start_hour, worst_case) - original_key = tuple(int(value) for value in individual) + original_key = self._fitness_key(individual) cached = self._fitness_cache.get(original_key) if cached is not None: individual[:] = cached.genome @@ -1855,10 +2191,10 @@ class GeneticOptimization(OptimizationBase): # persistent result for the remainder of the run. return fitness - canonical_key = tuple(int(value) for value in individual) + canonical_key = self._fitness_key(individual) extra_value1, extra_value2, extra_value3 = extra_data entry = FitnessCacheEntry( - genome=canonical_key, + genome=tuple(int(value) for value in individual), fitness=fitness, extra_data=( float(extra_value1), @@ -1870,6 +2206,18 @@ class GeneticOptimization(OptimizationBase): self._fitness_cache[canonical_key] = entry return fitness + def _fitness_key(self, individual: list[int]) -> tuple[int, ...]: + """Return the fitness-relevant genome, excluding elapsed control slots.""" + start_slot = self._start_day_slot() + relevant = list(individual[start_slot : self.total_slots]) + if self.optimize_ev: + ev_start = self.total_slots + start_slot + relevant.extend(individual[ev_start : self.total_slots * 2]) + n_appliance_genes = self.appliance_layout.n_genes + if n_appliance_genes > 0: + relevant.extend(individual[-n_appliance_genes:]) + return tuple(int(value) for value in relevant) + def _evaluate_uncached( self, individual: list[int], @@ -1918,7 +2266,7 @@ class GeneticOptimization(OptimizationBase): except Exception: # Return bad fitness score ("FitnessMin") in case of an exception if hasattr(individual, "extra_data"): - del individual.extra_data # type: ignore[attr-defined] + del individual.extra_data return (100000.0,) gesamtbilanz = simulation_result["Gesamtbilanz_Euro"] * (-1.0 if worst_case else 1.0) @@ -2192,11 +2540,13 @@ class GeneticOptimization(OptimizationBase): population.extend(self.toolbox.population(n=random_count)) logger.info( "Genetic settings: {} individuals, {} generations, {} survivors, " - "{} offspring per generation.", + "{} offspring per generation, adaptive mutation {:.0%}/{:.0%}.", individuals, ngen, individuals, individuals, + self.MUTATION_PROBABILITY, + self.STAGNATION_MUTATION_PROBABILITY, ) logger.info( "Initial population {}: {} exact warm starts, {} warm mutations, " @@ -2219,19 +2569,17 @@ class GeneticOptimization(OptimizationBase): local_improvements = 0 local_initial_fitness = float("nan") local_final_fitness = float("nan") + self._adaptive_evolution_metrics = {} try: - pop, log = algorithms.eaMuPlusLambda( + pop, log = self._evolve_population_adaptive( population, - self.toolbox, mu=individuals, lambda_=individuals, - cxpb=0.6, - mutpb=0.4, ngen=ngen, stats=stats, halloffame=hof, - verbose=self.verbose, ) + population = pop ( best_solution, local_evaluations, @@ -2245,6 +2593,9 @@ class GeneticOptimization(OptimizationBase): max(individuals, 1), ), ) + except Exception: + self._fitness_cache.clear() + raise finally: self._fitness_cache_enabled = False @@ -2260,12 +2611,13 @@ class GeneticOptimization(OptimizationBase): cache_lookups = self._fitness_cache_hits + self._fitness_cache_misses cache_hit_rate = self._fitness_cache_hits / cache_lookups if cache_lookups > 0 else 0.0 + cache_keys = len(self._fitness_cache) logger.info( "Fitness cache: {} hits, {} misses, {:.1%} hit rate, {} keys.", self._fitness_cache_hits, self._fitness_cache_misses, cache_hit_rate, - len(self._fitness_cache), + cache_keys, ) # Store fitness history @@ -2274,12 +2626,17 @@ class GeneticOptimization(OptimizationBase): "avg": log.select("avg"), # Average fitness for each generation (Y-axis) "max": log.select("max"), # Maximum fitness for each generation (Y-axis) "min": log.select("min"), # Minimum fitness for each generation (Y-axis) + "diversity": log.select("diversity"), + "stagnation": log.select("stagnation"), + "immigrants": log.select("immigrants"), + "restart": log.select("restart"), "fitness_cache": { "hits": self._fitness_cache_hits, "misses": self._fitness_cache_misses, "hit_rate": cache_hit_rate, - "keys": len(self._fitness_cache), + "keys": cache_keys, }, + "adaptive_evolution": self._adaptive_evolution_metrics, "local_search": { "evaluations": local_evaluations, "improvements": local_improvements, @@ -2296,6 +2653,9 @@ class GeneticOptimization(OptimizationBase): member["verluste"].append(extra_value2) member["nebenbedingung"].append(extra_value3) + # Avoid retaining large genome tuples in a long-lived API process until + # cyclic garbage collection happens. Cache statistics above are scalar. + self._fitness_cache.clear() return best_solution, member def optimierung_ems( diff --git a/tests/test_genetic_seeding.py b/tests/test_genetic_seeding.py index 53338d9e..fc2b39bf 100644 --- a/tests/test_genetic_seeding.py +++ b/tests/test_genetic_seeding.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock, patch import numpy as np import pytest -from deap import creator +from deap import creator, tools from akkudoktoreos.config.config import ConfigEOS from akkudoktoreos.core.coreabc import get_ems @@ -142,6 +142,34 @@ def test_fitness_cache_never_stores_failed_evaluations(config_eos: ConfigEOS): assert opt._fitness_cache == {} +def test_fitness_cache_ignores_elapsed_control_slots(config_eos: ConfigEOS): + _configure_hourly_grid(config_eos, start_hour=10) + opt = GeneticOptimization(fixed_seed=42) + opt.optimize_ev = False + opt.setup_deap_environment({"home_appliance": 0}, start_hour=10) + parameters = SimpleNamespace( + ems=SimpleNamespace(preis_euro_pro_wh_akku=0.0), + eauto=None, + ) + result = { + "Gesamtbilanz_Euro": 1.0, + "Gesamt_Verluste": 0.0, + "EAuto_SoC_pro_Stunde": np.zeros(opt.total_slots), + } + first = creator.Individual([0] * opt.total_slots) + elapsed_variant = creator.Individual(first) + elapsed_variant[0] = 1 + opt._fitness_cache_enabled = True + + with patch.object(opt, "evaluate_inner", return_value=result) as evaluate: + first_fitness = opt.evaluate(first, parameters, 10, False) # type: ignore[arg-type] + variant_fitness = opt.evaluate(elapsed_variant, parameters, 10, False) # type: ignore[arg-type] + + assert evaluate.call_count == 1 + assert first_fitness == variant_fitness + assert opt._fitness_cache_hits == 1 + + def test_mutated_warm_start_neighbors_keep_elapsed_slots(config_eos: ConfigEOS): _configure_hourly_grid(config_eos, start_hour=10) opt = GeneticOptimization(fixed_seed=42) @@ -170,7 +198,7 @@ def test_initial_population_uses_fixed_seed_budget_and_configured_population( educated = [[7] * opt.total_slots for _ in range(100)] captured: dict[str, object] = {} - def fake_ea(population, toolbox, **kwargs): + def fake_evolution(population, **kwargs): captured["population"] = list(population) captured["mu"] = kwargs["mu"] captured["lambda"] = kwargs["lambda_"] @@ -188,7 +216,7 @@ def test_initial_population_uses_fixed_seed_budget_and_configured_population( "population", side_effect=lambda n: [creator.Individual([9] * opt.total_slots) for _ in range(n)], ), - patch("akkudoktoreos.optimization.genetic.genetic.algorithms.eaMuPlusLambda", fake_ea), + patch.object(opt, "_evolve_population_adaptive", side_effect=fake_evolution), ): opt.optimize(start_solution=start_solution, ngen=1) @@ -220,7 +248,7 @@ def test_small_population_scales_warm_and_educated_seed_families(config_eos: Con captured["educated_count"] = count return [[7] * opt.total_slots for _ in range(count)] - def fake_ea(population, toolbox, **kwargs): + def fake_evolution(population, **kwargs): captured["population"] = list(population) captured["mu"] = kwargs["mu"] captured["lambda"] = kwargs["lambda_"] @@ -238,7 +266,7 @@ def test_small_population_scales_warm_and_educated_seed_families(config_eos: Con "population", side_effect=lambda n: [creator.Individual([9] * opt.total_slots) for _ in range(n)], ), - patch("akkudoktoreos.optimization.genetic.genetic.algorithms.eaMuPlusLambda", fake_ea), + patch.object(opt, "_evolve_population_adaptive", side_effect=fake_evolution), ): opt.optimize(start_solution=start_solution, ngen=1) @@ -255,6 +283,38 @@ def test_small_population_scales_warm_and_educated_seed_families(config_eos: Con assert captured["lambda"] == 100 +def test_adaptive_evolution_soft_restarts_collapsed_population(config_eos: ConfigEOS): + _configure_hourly_grid(config_eos) + opt = GeneticOptimization(fixed_seed=42) + opt.optimize_ev = False + opt.setup_deap_environment({"home_appliance": 0}, start_hour=0) + opt.toolbox.register("evaluate", lambda individual: (float(sum(individual)),)) + population = [creator.Individual([0] * opt.total_slots) for _ in range(20)] + stats = tools.Statistics(lambda individual: individual.fitness.values) + stats.register("min", np.min) + stats.register("avg", np.mean) + stats.register("max", np.max) + halloffame = tools.HallOfFame(1) + + fresh = [creator.Individual([value] + [0] * (opt.total_slots - 1)) for value in range(1, 20)] + with patch.object(opt, "_fresh_population", return_value=fresh) as create_fresh: + evolved, log = opt._evolve_population_adaptive( + population, + mu=20, + lambda_=20, + ngen=1, + stats=stats, + halloffame=halloffame, + ) + + create_fresh.assert_called_once_with(19, educated_fraction=0.40) + assert log.select("restart") == [0, 1] + assert log.select("immigrants") == [0, 19] + assert opt._adaptive_evolution_metrics["soft_restarts"] == 1 + assert opt._population_diversity(evolved) == pytest.approx(1.0) + assert halloffame[0].fitness.values == (0.0,) + + def test_local_search_moves_weak_export_to_later_expensive_import(config_eos: ConfigEOS): _configure_hourly_grid(config_eos) opt = GeneticOptimization(fixed_seed=42) diff --git a/tests/test_optimization_interval.py b/tests/test_optimization_interval.py index 35f02aef..15e186a2 100644 --- a/tests/test_optimization_interval.py +++ b/tests/test_optimization_interval.py @@ -227,8 +227,8 @@ def test_hourly_start_solution_is_expanded_to_slots(config_eos: ConfigEOS): assert migrated[:8] == [0, 0, 0, 0, 1, 1, 1, 1] -def test_quarter_hour_mutation_probability_preserves_hourly_rate(config_eos: ConfigEOS): - """A finer genome does not mutate four times as many controls per hour.""" +def test_quarter_hour_mutation_targets_three_future_controls(config_eos: ConfigEOS): + """Point mutation scales to roughly three effective future controls.""" config_eos.merge_settings_from_dict( { "prediction": {"hours": 48}, @@ -239,7 +239,28 @@ def test_quarter_hour_mutation_probability_preserves_hourly_rate(config_eos: Con opt.optimize_ev = False opt.setup_deap_environment({"home_appliance": 0}, start_hour=0) - assert opt.toolbox.mutate_charge_discharge.keywords["indpb"] == pytest.approx(0.05) + active_slots = opt.total_slots - opt._start_day_slot() + expected = min(0.10, opt.POINT_MUTATION_EXPECTED_GENES / active_slots) + assert opt.toolbox.mutate_charge_discharge.keywords["indpb"] == pytest.approx(expected) + + +def test_point_mutation_keeps_elapsed_slots_unchanged(config_eos: ConfigEOS): + config_eos.merge_settings_from_dict( + { + "prediction": {"hours": 48}, + "optimization": {"horizon_hours": 48, "interval": 900}, + } + ) + get_ems(init=True).set_start_datetime(to_datetime().set(hour=10, minute=0)) + opt = GeneticOptimization(fixed_seed=42) + opt.optimize_ev = False + opt.setup_deap_environment({"home_appliance": 0}, start_hour=10) + individual = [0] * opt.total_slots + + changed = opt._mutate_point_controls(individual) + + assert changed + assert individual[: opt._start_day_slot()] == [0] * opt._start_day_slot() def test_sub_hourly_home_appliance_is_scheduled(config_eos: ConfigEOS): @@ -253,9 +274,7 @@ def test_sub_hourly_home_appliance_is_scheduled(config_eos: ConfigEOS): parameters = load_hourly_parameters().model_copy( update={ "home_appliances": [ - HomeApplianceParameters( - device_id="dishwasher1", consumption_wh=1200, duration_h=2 - ) + HomeApplianceParameters(device_id="dishwasher1", consumption_wh=1200, duration_h=2) ] }, deep=True, diff --git a/tests/testdata/optimize_result_1.json b/tests/testdata/optimize_result_1.json index d0995b71..2333b345 100644 --- a/tests/testdata/optimize_result_1.json +++ b/tests/testdata/optimize_result_1.json @@ -116,24 +116,24 @@ 0, 0, 0, - 0, - 0, - 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, 1, 1, 1, 0, 0, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 0, 0, 0, 0, @@ -144,10 +144,10 @@ 0, 0, 1, - 1, - 1, 0, - 0 + 1, + 1, + 1 ], "battery_grid_export_allowed": [], "eautocharge_hours_float": null, @@ -239,9 +239,9 @@ 0.0, 0.022582049506752234, 0.3039575, - 0.19320652266312205, - 0.1358062627100041, - 0.0692592282596561, + 0.1926250109597104, + 0.13346423958241627, + 0.053398267180554584, 0.0, 0.0, 0.0, @@ -262,7 +262,7 @@ 0.0, 0.0, 0.0, - 0.09514375330616998, + 0.024351216461056053, 0.15236390731688437, 0.10316291465819699, 0.05435777788576338, @@ -272,10 +272,10 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 3421.6165248449383, - "Gesamtbilanz_Euro": 0.5951993628823129, - "Gesamteinnahmen_Euro": 1.1298399163065491, - "Gesamtkosten_Euro": 1.725039279188862, + "Gesamt_Verluste": 3807.1176630027076, + "Gesamtbilanz_Euro": 0.22702041221600888, + "Gesamteinnahmen_Euro": 1.0402628835513343, + "Gesamtkosten_Euro": 1.2672832957673432, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -324,14 +324,6 @@ 0.07557452231671152, 0.0, 4.55656845588237e-17, - 0.001414013162203277, - 0.005881449073870462, - 0.05258762370598476, - 0.0, - 0.0, - 0.0, - 0.26650619799999997, - 0.19588158, 0.0, 0.0, 0.0, @@ -341,6 +333,14 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25364873699864443, 0.1306329312971816, 0.07362195915902499, 0.060401289430882174, @@ -352,10 +352,10 @@ 0.013012984677295973, 0.08357424731947552, 0.0, + 0.19011028252189552, 0.0, 0.0, - 0.214398479, - 0.16484566 + 0.0 ], "Netzbezug_Wh_pro_Stunde": [ 439.1848126015972, @@ -364,14 +364,6 @@ 402.20607938643707, 0.0, 2.2737367544323206e-13, - 6.433180901743754, - 25.909467285772962, - 175.4675465665157, - 0.0, - 0.0, - 0.0, - 912.38, - 704.61, 0.0, 0.0, 0.0, @@ -381,6 +373,14 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 833.8222781020527, 537.58407941227, 322.9033296448464, 273.0618871197205, @@ -392,10 +392,10 @@ 57.32592368852852, 278.85968408233407, 0.0, + 617.0408390843736, 0.0, 0.0, - 733.99, - 592.97 + 0.0 ], "Netzeinspeisung_Wh_pro_Stunde": [ 0.0, @@ -404,9 +404,9 @@ 0.0, 322.60070723931767, 4342.25, - 2760.093180901744, - 1940.089467285773, - 989.4175465665157, + 2751.785870853006, + 1906.6319940345184, + 762.832388293637, 0.0, 0.0, 0.0, @@ -427,7 +427,7 @@ 0.0, 0.0, 0.0, - 1359.1964758024283, + 347.87452087222937, 2176.6272473840627, 1473.7559236885286, 776.5396840823341, @@ -444,14 +444,14 @@ 51.82392952637247, 543.9059151312817, 0.0, - 0.0, - 0.0, - 0.0, + 1.8741291469953731, + 7.548005965483245, + 51.11761170636123, 108.98319763338179, 106.80230977350088, 133.7321802766326, - 0.0, - 0.0, + 124.41545454545451, + 96.08318181818186, 70.41409090909087, 118.37045454545455, 94.68272727272722, @@ -460,22 +460,22 @@ 66.66681818181814, 69.12409090909085, 109.07302361034766, - 116.99491129525349, + 3.2918733722463145, 22.312089529472388, 54.18759955738153, 86.6234264543665, 165.15986945297027, 65.92300791736113, 538.2984000000001, - 278.8343903340726, + 400.1930249256966, 0.0, 0.0, 0.0, 111.78035844493081, - 90.18403329253945, + 6.04210069012484, 134.59227272727276, - 0.0, - 0.0 + 100.08954545454549, + 80.85954545454547 ], "akku_soc_pro_stunde": [ 80.0, @@ -491,31 +491,31 @@ 98.93741213017658, 95.76274429012544, 91.54148603150185, - 91.54148603150185, - 91.54148603150185, - 89.31881902048255, - 85.58237790478007, - 82.59365545298944, - 79.97317508108861, - 77.57859002599218, - 75.47420813893983, - 73.29226082489025, - 69.84937058394196, - 66.35170046539932, - 66.97148073010689, - 68.47669182892304, - 70.8828981193221, - 75.4706722707935, - 77.30186693516465, - 92.254600268498, + 87.61423052185997, + 84.5813018028517, + 82.35863479183242, + 78.62219367612994, + 75.6334712243393, + 73.01299085243846, + 70.61840579734205, + 68.5140239102897, + 66.33207659624011, + 62.88918635529183, + 62.98062728229866, + 63.600407547006235, + 65.10561864582239, + 67.51182493622146, + 72.09959908769285, + 73.93079375206398, + 88.88352708539732, 100.0, 100.0, 100.0, 100.0, 98.60068046043587, - 96.11252124341868, - 91.86402778611841, - 91.86402778611841 + 98.76851659071711, + 94.52002313341684, + 91.360630915786 ], "Electricity_price": [ 0.000228, @@ -709,15 +709,12 @@ "initial_soc_percentage": 54 }, "start_solution": [ + 2.0, + 2.0, + 1.0, + 1.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 2.0, 0.0, 0.0, 0.0, @@ -731,18 +728,21 @@ 1.0, 1.0, 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, 0.0, 0.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 0.0, 0.0, 0.0, 0.0, @@ -753,10 +753,10 @@ 0.0, 0.0, 1.0, - 1.0, - 1.0, 0.0, - 0.0 + 1.0, + 1.0, + 1.0 ], "washingstart": null, "appliance_starts": {} diff --git a/tests/testdata/optimize_result_1_be.json b/tests/testdata/optimize_result_1_be.json index d0995b71..c0384ba8 100644 --- a/tests/testdata/optimize_result_1_be.json +++ b/tests/testdata/optimize_result_1_be.json @@ -110,11 +110,11 @@ 0, 0, 0, - 0, - 0, - 0, - 0, - 0, + 1, + 1, + 1, + 1, + 1, 0, 0, 0, @@ -128,7 +128,7 @@ 1, 1, 1, - 1, + 0, 1, 1, 1, @@ -237,8 +237,8 @@ 0.0, 0.0, 0.0, - 0.022582049506752234, - 0.3039575, + 0.0, + 0.16427941326352855, 0.19320652266312205, 0.1358062627100041, 0.0692592282596561, @@ -262,7 +262,7 @@ 0.0, 0.0, 0.0, - 0.09514375330616998, + 0.14543003946319485, 0.15236390731688437, 0.10316291465819699, 0.05435777788576338, @@ -272,10 +272,10 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 3421.6165248449383, - "Gesamtbilanz_Euro": 0.5951993628823129, - "Gesamteinnahmen_Euro": 1.1298399163065491, - "Gesamtkosten_Euro": 1.725039279188862, + "Gesamt_Verluste": 3782.4922474084588, + "Gesamtbilanz_Euro": 0.5099857443907834, + "Gesamteinnahmen_Euro": 1.0178660662203505, + "Gesamtkosten_Euro": 1.5278518106111338, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -318,10 +318,10 @@ ], "home_appliance_energy_wh": {}, "Kosten_Euro_pro_Stunde": [ - 0.10013413727316416, - 0.09007999454232955, - 0.11436917344552285, - 0.07557452231671152, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 4.55656845588237e-17, 0.001414013162203277, @@ -336,7 +336,7 @@ 0.0, 0.0, 0.0, - 0.0, + 0.182970359, 0.0, 0.0, 0.0, @@ -358,10 +358,10 @@ 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ - 439.1848126015972, - 407.23324838304495, - 546.436566868241, - 402.20607938643707, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 2.2737367544323206e-13, 6.433180901743754, @@ -376,7 +376,7 @@ 0.0, 0.0, 0.0, - 0.0, + 556.31, 0.0, 0.0, 0.0, @@ -402,8 +402,8 @@ 0.0, 0.0, 0.0, - 322.60070723931767, - 4342.25, + 0.0, + 2346.848760907551, 2760.093180901744, 1940.089467285773, 989.4175465665157, @@ -427,7 +427,7 @@ 0.0, 0.0, 0.0, - 1359.1964758024283, + 2077.5719923313554, 2176.6272473840627, 1473.7559236885286, 776.5396840823341, @@ -438,12 +438,12 @@ 0.0 ], "Verluste_Pro_Stunde": [ - 37.96737751219166, - 46.38878980596536, - 39.91398802418894, - 51.82392952637247, - 543.9059151312817, - 0.0, + 97.85621559422765, + 101.92059640365329, + 114.42806532440363, + 106.67021307906845, + 582.6179999999995, + 239.44814869109382, 0.0, 0.0, 0.0, @@ -456,7 +456,7 @@ 118.37045454545455, 94.68272727272722, 83.01681818181817, - 75.86045454545456, + 0.0, 66.66681818181814, 69.12409090909085, 109.07302361034766, @@ -467,7 +467,7 @@ 165.15986945297027, 65.92300791736113, 538.2984000000001, - 278.8343903340726, + 192.62932835060133, 0.0, 0.0, 0.0, @@ -479,11 +479,11 @@ ], "akku_soc_pro_stunde": [ 80.0, - 81.05464937533866, - 82.3432268699488, - 83.45194875950959, - 84.8915023574644, - 100.0, + 79.16421888032488, + 78.69989843940196, + 77.45653455559739, + 77.16482920302518, + 93.3486625363585, 100.0, 100.0, 100.0, @@ -497,17 +497,17 @@ 85.58237790478007, 82.59365545298944, 79.97317508108861, - 77.57859002599218, - 75.47420813893983, - 73.29226082489025, - 69.84937058394196, - 66.35170046539932, - 66.97148073010689, - 68.47669182892304, - 70.8828981193221, - 75.4706722707935, - 77.30186693516465, - 92.254600268498, + 79.97317508108861, + 77.86879319403626, + 75.68684587998666, + 72.24395563903838, + 68.74628552049575, + 69.36606578520332, + 70.87127688401948, + 73.27748317441853, + 77.86525732588994, + 79.69645199026107, + 94.64918532359441, 100.0, 100.0, 100.0, @@ -719,11 +719,11 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, 0.0, 0.0, 0.0, @@ -737,7 +737,7 @@ 1.0, 1.0, 1.0, - 1.0, + 0.0, 1.0, 1.0, 1.0, diff --git a/tests/testdata/optimize_result_2.json b/tests/testdata/optimize_result_2.json index d60de8cb..392db83d 100644 --- a/tests/testdata/optimize_result_2.json +++ b/tests/testdata/optimize_result_2.json @@ -11,10 +11,7 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 1.0, 1.0, 0.0, 0.0, @@ -26,6 +23,9 @@ 0.0, 0.0, 0.0, + 1.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -110,23 +110,23 @@ 0, 0, 0, + 0, + 0, + 0, 1, - 1, - 1, - 1, + 0, 1, 0, 0, 0, - 0, - 1, - 1, 1, 0, 0, 1, - 1, 0, + 0, + 0, + 1, 1, 1, 1, @@ -135,17 +135,17 @@ 1, 0, 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, + 1, + 1, + 1, + 1, 1, 1, 0, + 1, + 1, + 1, + 1, 0, 0 ], @@ -161,37 +161,37 @@ 0.0, 0.0, 0.0, - 0.625, - 1.0, 0.0, - 0.375, + 0.0, + 0.0, 0.625, - 1.0, + 0.0, + 0.75, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.75, + 0.0, + 0.0, 0.625, + 0.0, + 0.0, + 0.0, + 0.5, 0.875, - 0.0, - 0.3, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 0.75, + 0.375, + 0.1, 0.0, 0.0, 0.0, @@ -202,37 +202,37 @@ ], "result": { "Last_Wh_pro_Stunde": [ - 7953.07, - 12103.91, + 1053.07, + 5677.336751616955, 1320.56, - 5272.03, - 8063.67, - 17059.173961709643, - 8116.22, - 10763.78, + 8032.03, + 1163.67, + 9456.82, + 1216.22, + 1103.78, 1129.12, - 4490.71, - 1050.98, - 988.56, + 1178.71, + 3550.98, + 3488.56, 912.38, - 704.61, + 2704.61, 516.37, 868.05, 694.34, 608.79, 556.31, - 488.89, + 8768.89, 506.91, 804.89, - 1141.98, + 8041.98, 1056.97, 992.46, 1155.99, - 827.01, - 1257.98, - 1232.67, - 3371.26, - 3360.88, + 6347.01, + 10917.98, + 9512.67, + 5011.26, + 1964.88, 1158.03, 1222.72, 1221.04, @@ -242,44 +242,44 @@ 592.97 ], "EAuto_SoC_pro_Stunde": [ + 5.0, + 5.0, + 5.0, 5.0, 15.925, - 33.405, - 33.405, - 39.96, - 50.885000000000005, - 68.365, - 79.29, - 94.585, - 94.585, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001 + 15.925, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 29.035, + 42.144999999999996, + 42.144999999999996, + 42.144999999999996, + 53.06999999999999, + 53.06999999999999, + 53.06999999999999, + 53.06999999999999, + 61.809999999999995, + 77.105, + 90.215, + 96.77, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518, + 98.518 ], "Einnahmen_Euro_pro_Stunde": [ 0.0, @@ -321,30 +321,11 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 8756.080717524794, - "Gesamtbilanz_Euro": 9.985237573040141, + "Gesamt_Verluste": 10997.311193085054, + "Gesamtbilanz_Euro": 9.400002543796743, "Gesamteinnahmen_Euro": 0.0, - "Gesamtkosten_Euro": 9.985237573040141, + "Gesamtkosten_Euro": 9.400002543796743, "Home_appliance_wh_per_hour": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, 0.0, 0.0, @@ -363,29 +344,29 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0 ], "home_appliance_energy_wh": { "dishwasher1": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, 0.0, 0.0, @@ -404,86 +385,105 @@ 0.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0 ] }, "Kosten_Euro_pro_Stunde": [ - 0.5980075076051746, - 1.473337992, + 0.10013413727316416, + 1.110569992, + 0.11436917344552285, + 0.4642292905233547, 0.0, 0.0, - 0.0, - 2.3442290999999997, - 0.9428514400845971, - 1.762926136273946, + 0.001414013162203277, + 0.005881449073870462, 0.05258762370598476, 0.0, + 0.995566611, + 1.033413892, + 0.0, + 0.7518815799999999, + 0.174739608, + 0.28801899, 0.0, 0.0, - 0.26650619799999997, - 0.19588158, + 0.0, + 1.4565879259999999, 0.0, 0.0, - 0.22802125600000003, - 0.0, - 0.0, - 0.08281196422730584, - 0.16677339, - 0.264113778992023, - 0.2536463762855701, + 1.0058038379999998, 0.1306329312971816, 0.07362195915902499, - 0.060401289430882174, - 0.009619897970888898, - 0.07029121023060134, - 4.179128154646605e-17, - 0.04064749825228731, - 0.1994538035279033, + 0.0, + 0.0, + 0.9554559982283223, + 0.1443477211581475, + 0.14848868609267263, + 0.0, 0.013012984677295973, - 0.08357424731947552, 0.0, 0.0, - 0.293043269, + 0.0, + 0.0, 0.214398479, 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ - 2622.8399456367306, - 6660.66, + 439.1848126015972, + 5020.66, + 546.436566868241, + 2470.61889581349, 0.0, 0.0, - 0.0, - 11697.75, - 4289.587989465865, - 7766.194432924872, + 6.433180901743754, + 25.909467285772962, 175.4675465665157, 0.0, + 3231.31, + 3480.68, + 0.0, + 2704.61, + 516.37, + 868.05, 0.0, 0.0, - 912.38, - 704.61, + 0.0, + 4368.889999999999, 0.0, 0.0, - 694.34, - 0.0, - 0.0, - 248.38621543882974, - 506.91, - 799.8600211751151, - 833.8145177040436, + 3306.3899999999994, 537.58407941227, 322.9033296448464, - 273.0618871197205, - 45.96224544141853, - 374.088399311343, - 2.2737367544323206e-13, - 202.83182760622412, - 907.4331370696236, + 0.0, + 0.0, + 5084.917499884632, + 785.3521281727285, + 740.9615074484663, + 0.0, 57.32592368852852, - 278.85968408233407, 0.0, 0.0, - 987.01, + 0.0, + 0.0, 733.99, 592.97 ], @@ -528,84 +528,84 @@ 0.0 ], "Verluste_Pro_Stunde": [ - 945.0059934764078, - 1152.0, - 114.42806532440363, - 768.1700560862765, - 752.877211603942, - 1152.0, - 362.1897587359037, - 485.44493195098454, + 37.96737751219166, + 599.9999999999998, + 39.91398802418894, + 945.0334674976189, + 582.6179999999995, + 1026.1905162926748, + 331.2111817082091, + 232.81073607429266, 118.73010558798194, - 641.287728412984, - 106.80230977350088, - 133.7321802766326, + 108.98319763338179, 0.0, 0.0, - 70.41409090909087, - 118.37045454545455, + 124.41545454545451, + 240.0, 0.0, + 0.0, + 94.68272727272722, 83.01681818181817, 75.86045454545456, - 32.79597062197777, - 0.0, - 0.0012025410138062752, - 3.292931608338464, + 1014.0, + 69.12409090909085, + 109.07302361034766, + 945.0, 22.312089529472388, 54.18759955738153, - 86.6234264543665, - 165.15986945297027, - 65.92300791736113, - 538.2984000000001, - 166.26381931274682, - 68.89237644835487, + 123.8591383343284, + 853.5941151729826, + 1083.022499986156, + 906.7055359846831, + 304.8583246953946, + 225.52229247529465, 176.85071084262336, - 93.18476208988011, + 131.21108264656203, 111.78035844493081, 90.18403329253945, - 0.0, + 134.59227272727276, 0.0, 0.0 ], "akku_soc_pro_stunde": [ 80.0, - 61.060772546061834, - 42.12137860666789, - 40.878014722863334, - 23.18290087405168, - 13.89226749676402, - 30.55893416343069, - 31.036427461650234, - 31.104342238066472, - 34.40240072662152, - 19.405325997784466, - 16.23065815773333, - 12.00939989910972, - 12.00939989910972, - 12.00939989910972, - 9.786732888090437, - 6.050291772387957, - 6.050291772387957, - 3.429811400487131, - 1.0352263453907122, - 0.0, - 0.0, - 3.3403917050174315e-05, - 0.09144092700684212, - 0.7112211917144087, - 2.216432290530563, - 4.622638580929631, - 9.210412732401027, - 11.04160739677217, - 25.994340730105503, - 30.612780155459586, - 32.526457279024996, - 37.438977024653425, - 40.027442638261206, - 38.62812309869707, - 36.139963881679876, - 36.139963881679876, - 36.139963881679876 + 81.05464937533866, + 97.72131604200533, + 98.83003793156611, + 79.89157364488383, + 96.07540697821715, + 78.84078381044188, + 88.04109441344768, + 94.50805930440028, + 97.80611779295532, + 96.7435299231319, + 96.7435299231319, + 96.7435299231319, + 92.81627441349004, + 99.48294108015669, + 99.48294108015669, + 99.48294108015669, + 96.49421862836606, + 93.87373825646522, + 91.4791532013688, + 72.53975926197485, + 70.35781194792527, + 66.91492170697698, + 47.975527767583046, + 48.59530803229061, + 50.10051913110676, + 51.331355728325214, + 33.27368862539725, + 14.334919685618749, + 1.0715355651189202, + 1.7753354997896456, + 5.155440452534742, + 10.06796019816317, + 11.456100036623162, + 10.05678049705903, + 7.568621280041836, + 3.32012782274156, + 3.32012782274156 ], "Electricity_price": [ 0.000228, @@ -702,37 +702,37 @@ 0.0, 0.0, 0.0, - 0.625, - 1.0, 0.0, - 0.375, + 0.0, + 0.0, 0.625, - 1.0, + 0.0, + 0.75, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.75, + 0.0, + 0.0, 0.625, + 0.0, + 0.0, + 0.0, + 0.5, 0.875, - 0.0, - 0.3, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 0.75, + 0.375, + 0.1, 0.0, 0.0, 0.0, @@ -795,53 +795,54 @@ "capacity_wh": 60000, "charging_efficiency": 0.95, "max_charge_power_w": 11040, - "soc_wh": 59897.4, + "soc_wh": 59110.8, "initial_soc_percentage": 5 }, "start_solution": [ - 0.0, - 0.0, - 1.0, 2.0, - 1.0, - 1.0, - 0.0, - 0.0, - 0.0, 2.0, 1.0, 1.0, 1.0, + 0.0, 1.0, + 2.0, + 2.0, + 0.0, + 0.0, + 2.0, + 2.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, 1.0, 2.0, 0.0, 0.0, - 0.0, 1.0, 1.0, 1.0, - 0.0, - 0.0, - 1.0, - 1.0, - 0.0, - 1.0, - 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 1.0, + 1.0, 1.0, 1.0, 0.0, @@ -853,54 +854,53 @@ 0.0, 0.0, 0.0, - 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 3.0, - 1.0, + 0.0, 4.0, - 3.0, - 6.0, 0.0, - 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 4.0, + 0.0, + 0.0, 3.0, - 6.0, - 3.0, - 5.0, + 0.0, + 0.0, 0.0, 2.0, - 3.0, - 3.0, 5.0, 4.0, - 6.0, - 3.0, - 0.0, - 0.0, 1.0, + 2.0, + 6.0, 5.0, - 0.0, - 3.0, 6.0, - 3.0, - 1.0, - 3.0, - 2.0, - 0.0, - 2.0, - 2.0, 4.0, + 5.0, 3.0, 1.0, - 5.0, - 5.0, - 3.0, - 0.0, - 2.0, - 29.0 + 10.0 ], - "washingstart": 39, + "washingstart": 20, "appliance_starts": { "dishwasher1": [ - "2025-01-16 15:00:00+01:00" + "2025-01-15 20:00:00+01:00" ] } } \ No newline at end of file diff --git a/tests/testdata/optimize_result_2_be.json b/tests/testdata/optimize_result_2_be.json index d60de8cb..91076309 100644 --- a/tests/testdata/optimize_result_2_be.json +++ b/tests/testdata/optimize_result_2_be.json @@ -15,7 +15,7 @@ 0.0, 0.0, 0.0, - 1.0, + 0.0, 0.0, 0.0, 0.0, @@ -110,23 +110,23 @@ 0, 0, 0, - 1, - 1, - 1, - 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, 1, 0, + 1, + 1, 0, 0, 0, 1, 1, 1, - 0, - 0, - 1, - 1, - 0, 1, 1, 1, @@ -143,10 +143,10 @@ 0, 0, 0, - 1, - 1, 0, 0, + 0, + 1, 0 ], "battery_grid_export_allowed": [], @@ -161,37 +161,37 @@ 0.0, 0.0, 0.0, - 0.625, - 1.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.625, + 0.5, + 0.75, 0.375, - 0.625, - 1.0, - 0.625, - 0.875, - 0.0, - 0.3, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, + 0.75, + 0.5, + 0.5, 0.0, + 0.5, + 0.5, + 0.4, 0.0, 0.0, 0.0, @@ -202,16 +202,16 @@ ], "result": { "Last_Wh_pro_Stunde": [ - 7953.07, - 12103.91, + 1053.07, + 1063.91, 1320.56, - 5272.03, - 8063.67, - 17059.173961709643, - 8116.22, - 10763.78, + 1132.03, + 1163.67, + 1176.82, + 1216.22, + 1103.78, 1129.12, - 4490.71, + 1178.71, 1050.98, 988.56, 912.38, @@ -221,19 +221,19 @@ 694.34, 608.79, 556.31, - 488.89, - 506.91, - 804.89, - 1141.98, + 7388.89, + 6026.91, + 9084.89, + 5281.98, 1056.97, - 992.46, - 1155.99, - 827.01, + 9272.46, + 6675.99, + 6347.01, 1257.98, - 1232.67, - 3371.26, - 3360.88, - 1158.03, + 6752.67, + 6391.26, + 7776.88, + 3658.0299999999997, 1222.72, 1221.04, 949.99, @@ -242,55 +242,55 @@ 592.97 ], "EAuto_SoC_pro_Stunde": [ + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, + 5.0, 5.0, 15.925, - 33.405, - 33.405, - 39.96, - 50.885000000000005, - 68.365, - 79.29, - 94.585, - 94.585, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001, - 99.82900000000001 + 24.665, + 37.775, + 44.330000000000005, + 44.330000000000005, + 57.440000000000005, + 66.18, + 74.92, + 74.92, + 83.66, + 92.4, + 99.392, + 99.392, + 99.392, + 99.392, + 99.392, + 99.392, + 99.392 ], "Einnahmen_Euro_pro_Stunde": [ 0.0, 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 0.022582049506752234, + 0.3039575, + 0.19320652266312205, + 0.13346423958241627, + 0.0692592282596561, 0.0, 0.0, 0.0, @@ -321,10 +321,10 @@ 0.0, 0.0 ], - "Gesamt_Verluste": 8756.080717524794, - "Gesamtbilanz_Euro": 9.985237573040141, - "Gesamteinnahmen_Euro": 0.0, - "Gesamtkosten_Euro": 9.985237573040141, + "Gesamt_Verluste": 7236.761593008068, + "Gesamtbilanz_Euro": 10.765839318494601, + "Gesamteinnahmen_Euro": 0.7224695400119466, + "Gesamtkosten_Euro": 11.488308858506548, "Home_appliance_wh_per_hour": [ 0.0, 0.0, @@ -355,9 +355,9 @@ 0.0, 0.0, 0.0, - 2500.0, - 2500.0, 0.0, + 2500.0, + 2500.0, 0.0, 0.0, 0.0, @@ -396,9 +396,9 @@ 0.0, 0.0, 0.0, - 2500.0, - 2500.0, 0.0, + 2500.0, + 2500.0, 0.0, 0.0, 0.0, @@ -408,83 +408,83 @@ ] }, "Kosten_Euro_pro_Stunde": [ - 0.5980075076051746, - 1.473337992, + 0.10013413727316416, + 0.09007999454232955, + 0.11436917344552285, + 0.07557452231671152, 0.0, + 4.55656845588237e-17, + 0.001414013162203277, 0.0, - 0.0, - 2.3442290999999997, - 0.9428514400845971, - 1.762926136273946, 0.05258762370598476, 0.0, 0.0, - 0.0, + 0.29116746986009023, 0.26650619799999997, 0.19588158, 0.0, 0.0, - 0.22802125600000003, 0.0, 0.0, - 0.08281196422730584, - 0.16677339, - 0.264113778992023, - 0.2536463762855701, + 0.0, + 0.9964959260000001, + 0.5352533899999999, + 1.5452864699999995, + 0.16621183799999983, 0.1306329312971816, - 0.07362195915902499, - 0.060401289430882174, - 0.009619897970888898, + 1.8585251365618425, + 1.1230913228365589, + 0.8820174288094884, 0.07029121023060134, - 4.179128154646605e-17, - 0.04064749825228731, - 0.1994538035279033, - 0.013012984677295973, + 0.3006368911258292, + 0.437965956045862, + 1.0551552313933648, + 0.2896168262092554, 0.08357424731947552, - 0.0, - 0.0, + 0.17784012884918773, + 0.19011028252189552, 0.293043269, - 0.214398479, + 0.0, 0.16484566 ], "Netzbezug_Wh_pro_Stunde": [ - 2622.8399456367306, - 6660.66, + 439.1848126015972, + 407.23324838304495, + 546.436566868241, + 402.20607938643707, 0.0, + 2.2737367544323206e-13, + 6.433180901743754, 0.0, - 0.0, - 11697.75, - 4289.587989465865, - 7766.194432924872, 175.4675465665157, 0.0, 0.0, - 0.0, + 980.6920507244535, 912.38, 704.61, 0.0, 0.0, - 694.34, 0.0, 0.0, - 248.38621543882974, - 506.91, - 799.8600211751151, - 833.8145177040436, + 0.0, + 2988.8900000000003, + 1626.9099999999999, + 4679.8499999999985, + 546.3899999999994, 537.58407941227, - 322.9033296448464, - 273.0618871197205, - 45.96224544141853, + 8151.426037551941, + 5077.266378103792, + 4214.130094646385, 374.088399311343, - 2.2737367544323206e-13, - 202.83182760622412, - 907.4331370696236, - 57.32592368852852, + 1635.6740540034234, + 2185.4588625043016, + 4800.5242556568, + 1275.8450493799796, 278.85968408233407, - 0.0, - 0.0, + 556.6201215937018, + 617.0408390843736, 987.01, - 733.99, + 0.0, 592.97 ], "Netzeinspeisung_Wh_pro_Stunde": [ @@ -492,11 +492,11 @@ 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 322.60070723931767, + 4342.25, + 2760.093180901744, + 1906.6319940345184, + 989.4175465665157, 0.0, 0.0, 0.0, @@ -528,84 +528,84 @@ 0.0 ], "Verluste_Pro_Stunde": [ - 945.0059934764078, - 1152.0, - 114.42806532440363, - 768.1700560862765, - 752.877211603942, - 1152.0, - 362.1897587359037, - 485.44493195098454, - 118.73010558798194, - 641.287728412984, + 37.96737751219166, + 46.38878980596536, + 39.91398802418894, + 51.82392952637247, + 543.9059151312817, + 0.0, + 0.0, + 7.548005965483245, + 0.0, + 108.98319763338179, 106.80230977350088, - 133.7321802766326, + 0.0014460869344160802, 0.0, 0.0, 70.41409090909087, 118.37045454545455, - 0.0, + 94.68272727272722, 83.01681818181817, 75.86045454545456, - 32.79597062197777, - 0.0, - 0.0012025410138062752, - 3.292931608338464, + 945.0, + 876.0, + 1014.0, + 807.0, 22.312089529472388, - 54.18759955738153, - 86.6234264543665, - 165.15986945297027, + 414.01032450623296, + 276.72796537245506, + 278.9400113575662, 65.92300791736113, - 538.2984000000001, - 166.26381931274682, - 68.89237644835487, - 176.85071084262336, + 348.1792864804107, + 317.77906350051614, + 226.9433106788162, + 23.073005925597585, 93.18476208988011, - 111.78035844493081, - 90.18403329253945, - 0.0, + 35.877614591244196, + 6.04210069012484, 0.0, + 100.08954545454549, 0.0 ], "akku_soc_pro_stunde": [ 80.0, - 61.060772546061834, - 42.12137860666789, - 40.878014722863334, - 23.18290087405168, - 13.89226749676402, - 30.55893416343069, - 31.036427461650234, - 31.104342238066472, - 34.40240072662152, - 19.405325997784466, - 16.23065815773333, - 12.00939989910972, - 12.00939989910972, - 12.00939989910972, - 9.786732888090437, - 6.050291772387957, - 6.050291772387957, - 3.429811400487131, - 1.0352263453907122, - 0.0, - 0.0, - 3.3403917050174315e-05, - 0.09144092700684212, - 0.7112211917144087, - 2.216432290530563, - 4.622638580929631, - 9.210412732401027, - 11.04160739677217, - 25.994340730105503, - 30.612780155459586, - 32.526457279024996, - 37.438977024653425, - 40.027442638261206, - 38.62812309869707, - 36.139963881679876, - 36.139963881679876, - 36.139963881679876 + 81.05464937533866, + 82.3432268699488, + 83.45194875950959, + 84.8915023574644, + 100.0, + 100.0, + 100.0, + 100.0, + 100.0, + 98.93741213017658, + 95.76274429012544, + 95.76278445920696, + 95.76278445920696, + 95.76278445920696, + 93.54011744818767, + 89.80367633248518, + 86.81495388069455, + 84.19447350879372, + 81.79988845369729, + 62.860494514303355, + 43.92110057490942, + 24.981706635515476, + 6.042312696121536, + 6.662092960829104, + 6.662379752668908, + 6.682601013014882, + 6.764267995169498, + 8.595462659540642, + 10.600442839552054, + 11.760972381233058, + 11.931619900089059, + 12.57253673135566, + 15.161002344963439, + 16.157602750275778, + 16.325438880557027, + 16.325438880557027, + 13.16604666292617 ], "Electricity_price": [ 0.000228, @@ -702,37 +702,37 @@ 0.0, 0.0, 0.0, - 0.625, - 1.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.625, + 0.5, + 0.75, 0.375, - 0.625, - 1.0, - 0.625, - 0.875, - 0.0, - 0.3, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, 0.0, + 0.75, + 0.5, + 0.5, 0.0, + 0.5, + 0.5, + 0.4, 0.0, 0.0, 0.0, @@ -795,37 +795,37 @@ "capacity_wh": 60000, "charging_efficiency": 0.95, "max_charge_power_w": 11040, - "soc_wh": 59897.4, + "soc_wh": 59635.2, "initial_soc_percentage": 5 }, "start_solution": [ 0.0, 0.0, - 1.0, - 2.0, - 1.0, - 1.0, 0.0, 0.0, - 0.0, - 2.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, 2.0, 0.0, 0.0, 0.0, 1.0, - 1.0, - 1.0, 0.0, 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, 1.0, 1.0, 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, 1.0, 1.0, 1.0, @@ -842,7 +842,9 @@ 0.0, 0.0, 0.0, - 1.0, + 0.0, + 0.0, + 0.0, 1.0, 0.0, 0.0, @@ -853,54 +855,52 @@ 0.0, 0.0, 0.0, - 2.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 3.0, - 1.0, + 2.0, 4.0, - 3.0, - 6.0, - 0.0, 1.0, - 3.0, - 6.0, - 3.0, - 5.0, 0.0, - 2.0, - 3.0, - 3.0, - 5.0, 4.0, - 6.0, - 3.0, - 0.0, - 0.0, - 1.0, - 5.0, - 0.0, - 3.0, - 6.0, - 3.0, - 1.0, - 3.0, + 2.0, 2.0, 0.0, 2.0, 2.0, 4.0, - 3.0, - 1.0, - 5.0, - 5.0, - 3.0, 0.0, + 3.0, 2.0, - 29.0 + 0.0, + 5.0, + 2.0, + 5.0, + 30.0 ], - "washingstart": 39, + "washingstart": 40, "appliance_starts": { "dishwasher1": [ - "2025-01-16 15:00:00+01:00" + "2025-01-16 16:00:00+01:00" ] } } \ No newline at end of file