Improve genetic optimizer convergence

This commit is contained in:
Andreas
2026-07-16 14:32:20 +02:00
parent 6465e22f07
commit 4dfd4b275b
9 changed files with 1084 additions and 665 deletions
+14 -4
View File
@@ -62,10 +62,15 @@ 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
- Use a fixed, diverse genetic start population with ten exact warm-start copies, up to fifty
locally mutated warm-start neighbours, up to one hundred randomized domain-informed battery,
direct-marketing, EV, and flexible-appliance schedules, and a guaranteed random remainder.
Retain 150 parents while generating 150 offspring per generation.
- 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
remainder. Survivor and offspring counts now follow `optimization.genetic.individuals` instead
of remaining fixed at 150.
- Add coherent battery block mutations and energy-shift mutations that move weak battery exports
into several later expensive self-consumption slots in one step. A bounded, fitness-checked
local search applies the same neighbourhood to the final incumbent, avoiding local minima that
cannot be crossed by an individually disadvantageous single-slot mutation.
- Memoize successful canonical fitness evaluations within one optimization run, including repaired
EV genomes and auxiliary metrics. Log cache hits, misses, key count, and hit rate after each run;
failed evaluations and results from previous runs are never reused.
@@ -80,6 +85,11 @@ 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
- 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
generated 150 offspring. The deprecated `?ngen=` query remains a generation-count override;
`?individuals=` can override the population for one API run.
- Allow the direct-marketing optimizer to select a true battery self-consumption state with DC
charging and local-load discharge enabled in the same slot. Existing warm-start state numbers
remain compatible, and educated guesses now use the combined state for PV/load overlap instead
+13 -5
View File
@@ -159,6 +159,7 @@ class EnergyManagement(
mode: EnergyManagementMode,
genetic_parameters: Optional[GeneticOptimizationParameters] = None,
genetic_individuals: Optional[int] = None,
genetic_generations: Optional[int] = None,
genetic_seed: Optional[int] = None,
force_enable: Optional[bool] = False,
force_update: Optional[bool] = False,
@@ -180,8 +181,9 @@ class EnergyManagement(
parameter set for the genetic algorithm. If not provided, it will
be constructed based on the current configuration and predictions.
genetic_individuals (int, optional): The number of individuals for the
genetic algorithm. Defaults to the algorithm's internal default (400)
if not specified.
initial genetic population. Defaults to the configured value.
genetic_generations (int, optional): The number of generations to
evolve. Defaults to the configured value.
genetic_seed (int, optional): The seed for the genetic algorithm. Defaults
to the algorithm's internal random seed if not specified.
force_enable (bool, optional): If True, bypasses any disabled state
@@ -248,6 +250,8 @@ class EnergyManagement(
# Take values from config if not given
if genetic_individuals is None:
genetic_individuals = cls.config.optimization.genetic.individuals
if genetic_generations is None:
genetic_generations = cls.config.optimization.genetic.generations
if genetic_seed is None:
genetic_seed = cls.config.optimization.genetic.seed
@@ -262,7 +266,8 @@ class EnergyManagement(
solution = optimization.optimierung_ems(
start_hour=cls._start_datetime.hour,
parameters=genetic_parameters,
ngen=genetic_individuals,
ngen=genetic_generations,
individuals=genetic_individuals,
)
except:
logger.exception("Energy management optimization failed.")
@@ -305,6 +310,7 @@ class EnergyManagement(
mode: Optional[EnergyManagementMode] = None,
genetic_parameters: Optional[GeneticOptimizationParameters] = None,
genetic_individuals: Optional[int] = None,
genetic_generations: Optional[int] = None,
genetic_seed: Optional[int] = None,
force_enable: Optional[bool] = False,
force_update: Optional[bool] = False,
@@ -328,8 +334,9 @@ class EnergyManagement(
parameter set for the genetic algorithm. If not provided, it will
be constructed based on the current configuration and predictions.
genetic_individuals (int, optional): The number of individuals for the
genetic algorithm. Defaults to the algorithm's internal default (400)
if not specified.
initial genetic population. Defaults to the configured value.
genetic_generations (int, optional): The number of generations to
evolve. Defaults to the configured value.
genetic_seed (int, optional): The seed for the genetic algorithm. Defaults
to the algorithm's internal random seed if not specified.
force_enable (bool, optional): If True, bypasses any disabled state
@@ -354,6 +361,7 @@ class EnergyManagement(
mode=mode,
genetic_parameters=genetic_parameters,
genetic_individuals=genetic_individuals,
genetic_generations=genetic_generations,
genetic_seed=genetic_seed,
force_enable=force_enable,
force_update=force_update,
+311 -60
View File
@@ -180,6 +180,7 @@ class GeneticSimulation(PydanticBaseModel):
ev_discharge_hours: Optional[NDArray[Shape["*"], float]] = Field(
default=None, json_schema_extra={"description": "TBD"}
)
def prepare(
self,
parameters: GeneticEnergyManagementParameters,
@@ -562,8 +563,13 @@ class GeneticOptimization(OptimizationBase):
WARM_START_MUTATIONS = 50
EDUCATED_GUESS_TARGET = 100
MIN_RANDOM_POPULATION_FRACTION = 0.25
SURVIVOR_COUNT = 150
OFFSPRING_COUNT = 150
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)
# Slot-math helpers — single source of truth for the optimization grid.
@@ -787,9 +793,7 @@ class GeneticOptimization(OptimizationBase):
gene_index += 1
return ApplianceGeneLayout(genes)
def _decode_appliance_starts(
self, appliance_gene_values: list[int]
) -> dict[int, list[int]]:
def _decode_appliance_starts(self, appliance_gene_values: list[int]) -> dict[int, list[int]]:
"""Map appliance gene values to absolute start slots per appliance.
Each gene value is an index into its gene's ``allowed_start_slots``; it is
@@ -1053,6 +1057,95 @@ class GeneticOptimization(OptimizationBase):
return ac_charge, dc_charge, discharge, battery_grid_export
def _mutate_battery_block(self, individual: list[int]) -> None:
"""Mutate a short future block to one coherent operating policy."""
start_slot = self._start_day_slot()
if start_slot >= self.total_slots:
return
state_layout = self._battery_state_layout()
len_bat = len(self.bat_possible_charge_values)
policy_states = [0, len_bat]
if state_layout.self_consumption_state is not None:
policy_states.append(state_layout.self_consumption_state)
if state_layout.dc_allowed_state is not None:
policy_states.append(state_layout.dc_allowed_state)
if state_layout.grid_export_state is not None:
policy_states.append(state_layout.grid_export_state)
block_start = random.randint(start_slot, self.total_slots - 1) # noqa: S311
max_length = min(12, self.total_slots - block_start)
block_length = random.randint(2, max(2, max_length)) if max_length > 1 else 1 # noqa: S311
state = random.choice(policy_states) # noqa: S311
individual[block_start : block_start + block_length] = [state] * block_length
def _energy_shift_target_slots(
self,
individual: list[int],
source_slot: int,
) -> list[int]:
"""Return later idle slots where retained battery energy avoids costly import."""
try:
prices = np.asarray(self.simulation.elect_price_hourly, dtype=float)
feed_in = np.asarray(self.simulation.elect_revenue_per_hour_arr, dtype=float)
pv = np.asarray(self.simulation.pv_prediction_wh, dtype=float)
load = np.asarray(self.simulation.load_energy_array, dtype=float)
except Exception:
return []
if any(values.size < self.total_slots for values in (prices, feed_in, pv, load)):
return []
len_bat = len(self.bat_possible_charge_values)
source_tariff = float(feed_in[source_slot])
candidates = [
slot
for slot in range(source_slot + 1, self.total_slots)
if 0 <= int(individual[slot]) < len_bat
and load[slot] > pv[slot]
and prices[slot] > source_tariff
]
return sorted(
candidates,
key=lambda slot: (float(prices[slot]), float(load[slot] - pv[slot])),
reverse=True,
)
def _mutate_energy_shift(self, individual: list[int]) -> bool:
"""Move battery energy from a weak export into later expensive self-consumption."""
state_layout = self._battery_state_layout()
export_state = state_layout.grid_export_state
self_state = state_layout.self_consumption_state
if export_state is None or self_state is None:
return False
start_slot = self._start_day_slot()
viable: list[tuple[int, list[int]]] = []
for source_slot in range(start_slot, self.total_slots):
if int(individual[source_slot]) != export_state:
continue
targets = self._energy_shift_target_slots(individual, source_slot)
if targets:
viable.append((source_slot, targets))
if not viable:
return False
# Prefer later/lower-value exports, but retain random diversity among
# the viable tail instead of always producing one identical neighbour.
try:
feed_in = np.asarray(self.simulation.elect_revenue_per_hour_arr, dtype=float)
viable.sort(key=lambda item: (float(feed_in[item[0]]), -item[0]))
except Exception:
viable.sort(key=lambda item: -item[0])
source_slot, targets = random.choice(viable[: min(6, len(viable))]) # noqa: S311
individual[source_slot] = self_state
target_count = min(len(targets), random.randint(4, 10)) # noqa: S311
len_bat = len(self.bat_possible_charge_values)
pv = np.asarray(self.simulation.pv_prediction_wh, dtype=float)
for target_slot in targets[:target_count]:
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."""
total_states = self._battery_state_layout().total_states
@@ -1065,6 +1158,15 @@ class GeneticOptimization(OptimizationBase):
charge_discharge_mutated = np.clip(charge_discharge_mutated, 0, total_states - 1)
individual[: self.total_slots] = charge_discharge_mutated
# 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
if self.optimize_ev:
ev_charge_part = individual[self.total_slots : self.total_slots * 2]
@@ -1214,10 +1316,7 @@ class GeneticOptimization(OptimizationBase):
for offset in range(result_slots):
slot = start_slot + offset
charge_index = int(ev_charge_indices[slot])
if (
ev_soc[offset] >= 100.0 - 1e-9
and ev_possible_charge_values[charge_index] > 0.0
):
if ev_soc[offset] >= 100.0 - 1e-9 and ev_possible_charge_values[charge_index] > 0.0:
ev_charge_indices[slot] = zero_charge_index
changed = True
@@ -1245,8 +1344,7 @@ class GeneticOptimization(OptimizationBase):
return schedule
required_stored_wh = max(
ev.min_soc_wh
- ev.capacity_wh * ev.initial_soc_percentage / 100.0,
ev.min_soc_wh - ev.capacity_wh * ev.initial_soc_percentage / 100.0,
0.0,
)
if required_stored_wh <= 0.0:
@@ -1277,11 +1375,7 @@ class GeneticOptimization(OptimizationBase):
if not positive_rates:
return schedule
max_stored_wh = (
ev.max_charge_power_w
* self.slot_duration_h
* ev.charging_efficiency
)
max_stored_wh = ev.max_charge_power_w * self.slot_duration_h * ev.charging_efficiency
remaining_wh = required_stored_wh
for slot in candidates:
required_rate = remaining_wh / max(max_stored_wh, 1e-9)
@@ -1305,6 +1399,7 @@ class GeneticOptimization(OptimizationBase):
load = np.asarray(self.simulation.load_energy_array, dtype=float)
genes: list[int] = []
for gene in self.appliance_layout.genes:
def opportunity_cost(position: int) -> float:
slot = gene.allowed_start_slots[position]
return float(feed_in[slot] if pv[slot] > load[slot] else prices[slot])
@@ -1417,15 +1512,23 @@ class GeneticOptimization(OptimizationBase):
# feed-in slots. At low tariffs PV is preferentially stored instead.
if self.optimize_battery_grid_export and future_feed_in.size:
for quantile in self.EDUCATED_GUESS_EXPORT_QUANTILES:
export_guess = policy_guess(
import_quantile=0.70,
export_quantile=quantile,
pv_surplus_ratio=1.0,
allow_ac_arbitrage=False,
)
add_guess(
policy_guess(
import_quantile=0.70,
export_quantile=quantile,
pv_surplus_ratio=1.0,
allow_ac_arbitrage=False,
),
export_guess,
ev_pv,
)
# Seed coordinated alternatives that retain a weak export and
# spend the energy in later expensive import slots.
for shifted in self._grid_export_shift_candidates(
export_guess,
max_sources=2,
)[:6]:
add_guess(shifted, ev_pv)
inverter = self.simulation.inverter
ac_arbitrage_possible = inverter is not None and (
@@ -1473,6 +1576,9 @@ class GeneticOptimization(OptimizationBase):
elif load[slot] > pv[slot] and prices[slot] >= high_import_price:
randomized[slot] = discharge_state
if random.random() < 0.5: # noqa: S311
self._mutate_energy_shift(randomized)
add_guess(
randomized,
ev_pv if random.random() < 0.5 else ev_price, # noqa: S311
@@ -1510,6 +1616,104 @@ class GeneticOptimization(OptimizationBase):
break
return neighbors
def _grid_export_shift_candidates(
self,
individual: list[int],
*,
max_sources: int = 6,
) -> list[list[int]]:
"""Build deterministic export-to-self-consumption neighbourhood candidates."""
state_layout = self._battery_state_layout()
export_state = state_layout.grid_export_state
self_state = state_layout.self_consumption_state
if export_state is None or self_state is None:
return []
start_slot = self._start_day_slot()
try:
feed_in = np.asarray(self.simulation.elect_revenue_per_hour_arr, dtype=float)
pv = np.asarray(self.simulation.pv_prediction_wh, dtype=float)
except Exception:
return []
if feed_in.size < self.total_slots or pv.size < self.total_slots:
return []
sources = [
slot
for slot in range(start_slot, self.total_slots)
if int(individual[slot]) == export_state
]
# Search weak and late export decisions first. They are the most likely
# to compete with later, more valuable avoided grid imports.
sources.sort(key=lambda slot: (float(feed_in[slot]), -slot))
len_bat = len(self.bat_possible_charge_values)
candidates: list[list[int]] = []
seen: set[tuple[int, ...]] = set()
viable_sources = 0
for source_slot in sources:
targets = self._energy_shift_target_slots(individual, source_slot)
if not targets:
continue
viable_sources += 1
counts = sorted({min(len(targets), count) for count in (2, 4, 6, 8, 10, 12)})
for count in counts:
candidate = list(individual)
candidate[source_slot] = self_state
for target_slot in targets[:count]:
candidate[target_slot] = self_state if pv[target_slot] > 0.0 else len_bat
key = tuple(int(value) for value in candidate)
if key in seen:
continue
seen.add(key)
candidates.append(candidate)
if viable_sources >= max_sources:
break
return candidates
def _locally_improve_grid_export(
self,
individual: list[int],
*,
max_evaluations: int,
) -> tuple[Any, int, int, float, float]:
"""Improve the incumbent through bounded, fitness-checked energy shifts."""
best = creator.Individual(individual)
original_fitness = getattr(individual, "fitness", None)
if original_fitness is not None and original_fitness.valid:
best.fitness.values = original_fitness.values
if hasattr(individual, "extra_data"):
best.extra_data = individual.extra_data
if not hasattr(self.toolbox, "evaluate"):
value = float(best.fitness.values[0]) if best.fitness.valid else float("inf")
return best, 0, 0, value, value
if not best.fitness.valid:
best.fitness.values = self.toolbox.evaluate(best)
initial_value = float(best.fitness.values[0])
evaluations = 0
improvements = 0
for _ in range(self.LOCAL_SEARCH_MAX_PASSES):
pass_best = best
for genome in self._grid_export_shift_candidates(best):
if evaluations >= max_evaluations:
break
candidate = creator.Individual(genome)
candidate.fitness.values = self.toolbox.evaluate(candidate)
evaluations += 1
if candidate.fitness.values[0] < pass_best.fitness.values[0] - 1e-9:
pass_best = candidate
if pass_best is best:
break
best = pass_best
improvements += 1
if evaluations >= max_evaluations:
break
final_value = float(best.fitness.values[0])
return best, evaluations, improvements, initial_value, final_value
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
@@ -1892,6 +2096,7 @@ class GeneticOptimization(OptimizationBase):
self,
start_solution: Optional[list[float]] = None,
ngen: int = 200,
individuals: Optional[int] = None,
) -> tuple[Any, dict[str, list[Any]]]:
"""Run the optimization process using a genetic algorithm.
@@ -1904,13 +2109,14 @@ class GeneticOptimization(OptimizationBase):
random.seed(self.fix_seed)
# Set the number of inviduals in a generation
try:
individuals = self.config.optimization.genetic.individuals
if individuals is None:
raise
except:
individuals = 300
logger.error("Individuals not configured. Using {}.", individuals)
if individuals is None:
try:
individuals = self.config.optimization.genetic.individuals
if individuals is None:
raise ValueError("individuals is not configured")
except Exception:
individuals = 300
logger.error("Individuals not configured. Using {}.", individuals)
hof = tools.HallOfFame(1)
stats = tools.Statistics(lambda ind: ind.fitness.values)
@@ -1924,9 +2130,7 @@ class GeneticOptimization(OptimizationBase):
valid_start_solution: Optional[list[float]] = None
if start_solution is not None:
n_appliance_genes = self.appliance_layout.n_genes
expected_length = (
self.total_slots * (2 if self.optimize_ev else 1) + n_appliance_genes
)
expected_length = self.total_slots * (2 if self.optimize_ev else 1) + n_appliance_genes
start_solution = self._start_solution_for_slot_grid(start_solution)
if len(start_solution) != expected_length:
@@ -1943,42 +2147,57 @@ class GeneticOptimization(OptimizationBase):
else:
valid_start_solution = start_solution
# Keep the configured initial population size fixed. With the default
# 300 individuals this yields 10 exact warm starts, 50 local variants,
# 100 educated guesses and 140 fully random candidates.
# Scale the seed families with small populations without changing the
# established 300-individual defaults. This prevents a 100-member run
# from spending 60% of its budget on the warm-start neighbourhood.
exact_warm_target = min(
self.WARM_START_COPIES,
max(1, int(individuals * self.WARM_START_COPY_FRACTION + 0.999999)),
)
warm_mutation_target = min(
self.WARM_START_MUTATIONS,
max(1, int(individuals * self.WARM_START_MUTATION_FRACTION + 0.999999)),
)
educated_guess_target = min(
self.EDUCATED_GUESS_TARGET,
max(1, int(individuals * self.EDUCATED_GUESS_FRACTION + 0.999999)),
)
minimum_random = max(
int(individuals * self.MIN_RANDOM_POPULATION_FRACTION + 0.999999),
individuals
- (
self.WARM_START_COPIES
+ self.WARM_START_MUTATIONS
+ self.EDUCATED_GUESS_TARGET
),
individuals - (exact_warm_target + warm_mutation_target + educated_guess_target),
)
seed_budget = max(individuals - minimum_random, 0)
seeded: list[list[float]] = []
seeded: list[list[Any]] = []
exact_warm_count = 0
warm_neighbors: list[list[int]] = []
if valid_start_solution is not None and seed_budget > 0:
exact_warm_count = min(self.WARM_START_COPIES, seed_budget)
exact_warm_count = min(exact_warm_target, seed_budget)
seeded.extend([valid_start_solution] * exact_warm_count)
remaining_seed_budget = seed_budget - len(seeded)
warm_neighbors = self._mutated_warm_start_neighbors(
valid_start_solution,
min(self.WARM_START_MUTATIONS, remaining_seed_budget),
min(warm_mutation_target, remaining_seed_budget),
)
seeded.extend(warm_neighbors)
remaining_seed_budget = seed_budget - len(seeded)
educated_guesses = self._educated_guess_individuals(
min(self.EDUCATED_GUESS_TARGET, remaining_seed_budget)
min(educated_guess_target, remaining_seed_budget)
)
seeded.extend(educated_guesses)
random_count = max(individuals - len(seeded), 0)
population = [creator.Individual(seed) for seed in seeded]
population.extend(self.toolbox.population(n=random_count))
logger.info(
"Genetic settings: {} individuals, {} generations, {} survivors, "
"{} offspring per generation.",
individuals,
ngen,
individuals,
individuals,
)
logger.info(
"Initial population {}: {} exact warm starts, {} warm mutations, "
"{} educated guesses, {} random candidates.",
@@ -1996,12 +2215,16 @@ class GeneticOptimization(OptimizationBase):
self._fitness_cache_hits = 0
self._fitness_cache_misses = 0
self._fitness_cache_enabled = True
local_evaluations = 0
local_improvements = 0
local_initial_fitness = float("nan")
local_final_fitness = float("nan")
try:
pop, log = algorithms.eaMuPlusLambda(
population,
self.toolbox,
mu=self.SURVIVOR_COUNT,
lambda_=self.OFFSPRING_COUNT,
mu=individuals,
lambda_=individuals,
cxpb=0.6,
mutpb=0.4,
ngen=ngen,
@@ -2009,13 +2232,34 @@ class GeneticOptimization(OptimizationBase):
halloffame=hof,
verbose=self.verbose,
)
(
best_solution,
local_evaluations,
local_improvements,
local_initial_fitness,
local_final_fitness,
) = self._locally_improve_grid_export(
hof[0],
max_evaluations=min(
self.LOCAL_SEARCH_MAX_EVALUATIONS,
max(individuals, 1),
),
)
finally:
self._fitness_cache_enabled = False
if local_improvements:
logger.info(
"Grid-export local search: {} improvements in {} evaluations, "
"fitness {:.6f} -> {:.6f}.",
local_improvements,
local_evaluations,
local_initial_fitness,
local_final_fitness,
)
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_hit_rate = self._fitness_cache_hits / cache_lookups if cache_lookups > 0 else 0.0
logger.info(
"Fitness cache: {} hits, {} misses, {:.1%} hit rate, {} keys.",
self._fitness_cache_hits,
@@ -2036,6 +2280,12 @@ class GeneticOptimization(OptimizationBase):
"hit_rate": cache_hit_rate,
"keys": len(self._fitness_cache),
},
"local_search": {
"evaluations": local_evaluations,
"improvements": local_improvements,
"initial_fitness": local_initial_fitness,
"final_fitness": local_final_fitness,
},
}
member: dict[str, list[float]] = {"bilanz": [], "verluste": [], "nebenbedingung": []}
@@ -2046,7 +2296,7 @@ class GeneticOptimization(OptimizationBase):
member["verluste"].append(extra_value2)
member["nebenbedingung"].append(extra_value3)
return hof[0], member
return best_solution, member
def optimierung_ems(
self,
@@ -2054,6 +2304,7 @@ class GeneticOptimization(OptimizationBase):
start_hour: Optional[int] = None,
worst_case: bool = False,
ngen: Optional[int] = None,
individuals: Optional[int] = None,
) -> GeneticSolution:
"""Perform EMS (Energy Management System) optimization and visualize results."""
direct_marketing_enabled = self._direct_marketing_enabled()
@@ -2177,9 +2428,7 @@ class GeneticOptimization(OptimizationBase):
)
for appliance_params in home_appliance_params
]
self.appliance_layout = self._build_appliance_layout(
home_appliances, self._slot0_datetime
)
self.appliance_layout = self._build_appliance_layout(home_appliances, self._slot0_datetime)
# Initialize the inverter and energy management system. slot_duration_h
# lets the Inverter scale max_power_wh to a per-slot energy cap.
@@ -2205,16 +2454,18 @@ class GeneticOptimization(OptimizationBase):
# Setup the DEAP environment and optimization process. The appliance
# genome layout (built above) drives the appliance gene block; evaluate
# gets the slot index (its break-even loop walks the slot arrays from "now").
self.setup_deap_environment(
{"home_appliance": self.appliance_layout.n_genes}, start_hour
)
self.setup_deap_environment({"home_appliance": self.appliance_layout.n_genes}, start_hour)
self.toolbox.register(
"evaluate",
lambda ind: self.evaluate(ind, parameters, start_slot, worst_case),
)
start_time = time.time()
start_solution, extra_data = self.optimize(parameters.start_solution, ngen=generations)
start_solution, extra_data = self.optimize(
parameters.start_solution,
ngen=generations,
individuals=individuals,
)
elapsed_time = time.time() - start_time
logger.debug(f"Time evaluate inner: {elapsed_time:.4f} sec.")
@@ -2222,8 +2473,8 @@ class GeneticOptimization(OptimizationBase):
simulation_result = self.evaluate_inner(start_solution)
# Prepare results
discharge_hours_bin, eautocharge_hours_index, appliance_gene_values = (
self.split_individual(start_solution)
discharge_hours_bin, eautocharge_hours_index, appliance_gene_values = self.split_individual(
start_solution
)
# Materialize the per-device appliance results only for the final best
+17 -2
View File
@@ -1408,7 +1408,21 @@ async def fastapi_optimize(
Optional[int], Query(description="Defaults to current hour of the day.")
] = None,
ngen: Annotated[
Optional[int], Query(description="Number of indivuals to generate for genetic algorithm.")
Optional[int],
Query(
description=(
"Deprecated alias for the number of genetic generations. "
"Defaults to optimization.genetic.generations."
),
ge=1,
),
] = None,
individuals: Annotated[
Optional[int],
Query(
description="Override optimization.genetic.individuals for this run.",
ge=10,
),
] = None,
) -> GeneticSolution:
"""Deprecated: Optimize.
@@ -1429,7 +1443,8 @@ async def fastapi_optimize(
start_datetime=start_datetime,
mode=EnergyManagementMode.OPTIMIZATION,
genetic_parameters=parameters,
genetic_individuals=ngen,
genetic_individuals=individuals,
genetic_generations=ngen,
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Optimize error: {e}.")
+1 -1
View File
@@ -430,7 +430,7 @@ def run_optimization(
start_datetime=start_datetime,
mode=EnergyManagementMode.OPTIMIZATION,
genetic_parameters=parameters,
genetic_individuals=ngen,
genetic_generations=ngen,
genetic_seed=seed,
)
)
+140 -5
View File
@@ -1,5 +1,5 @@
from types import SimpleNamespace
from unittest.mock import patch
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
@@ -7,6 +7,7 @@ from deap import creator
from akkudoktoreos.config.config import ConfigEOS
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.core.emsettings import EnergyManagementMode
from akkudoktoreos.optimization.genetic.genetic import GeneticOptimization
from akkudoktoreos.utils.datetimeutil import to_datetime
@@ -21,6 +22,36 @@ def _configure_hourly_grid(config_eos: ConfigEOS, *, start_hour: int = 0) -> Non
get_ems(init=True).set_start_datetime(to_datetime().set(hour=start_hour, minute=0))
def test_energy_management_forwards_individuals_and_generations_separately(
config_eos: ConfigEOS,
):
_configure_hourly_grid(config_eos)
config_eos.optimization.genetic.individuals = 100
config_eos.optimization.genetic.generations = 80
ems = get_ems(init=True)
parameters = MagicMock()
solution = MagicMock()
optimizer = MagicMock()
optimizer.optimierung_ems.return_value = solution
with (
patch("akkudoktoreos.adapter.adapterabc.AdapterContainer.update_data"),
patch("akkudoktoreos.core.ems.GeneticOptimization", return_value=optimizer),
):
ems._run(
start_datetime=to_datetime().set(hour=0, minute=0),
mode=EnergyManagementMode.OPTIMIZATION,
genetic_parameters=parameters,
)
optimizer.optimierung_ems.assert_called_once_with(
start_hour=0,
parameters=parameters,
ngen=80,
individuals=100,
)
def test_ev_repair_is_resimulated_before_fitness_assignment(config_eos: ConfigEOS):
_configure_hourly_grid(config_eos)
opt = GeneticOptimization(fixed_seed=42)
@@ -44,7 +75,9 @@ def test_ev_repair_is_resimulated_before_fitness_assignment(config_eos: ConfigEO
eauto=None,
)
with patch.object(opt, "evaluate_inner", side_effect=[first_result, repaired_result]) as evaluate:
with patch.object(
opt, "evaluate_inner", side_effect=[first_result, repaired_result]
) as evaluate:
fitness = opt.evaluate(individual, parameters, start_hour=0, worst_case=False) # type: ignore[arg-type]
assert evaluate.call_count == 2
@@ -124,7 +157,9 @@ def test_mutated_warm_start_neighbors_keep_elapsed_slots(config_eos: ConfigEOS):
assert all(neighbor != start_solution for neighbor in neighbors)
def test_initial_population_uses_fixed_seed_budget_and_150_survivors(config_eos: ConfigEOS):
def test_initial_population_uses_fixed_seed_budget_and_configured_population(
config_eos: ConfigEOS,
):
_configure_hourly_grid(config_eos)
config_eos.optimization.genetic.individuals = 300
opt = GeneticOptimization(fixed_seed=42)
@@ -164,8 +199,108 @@ def test_initial_population_uses_fixed_seed_budget_and_150_survivors(config_eos:
assert first_genes.count(6) == 50
assert first_genes.count(7) == 100
assert first_genes.count(9) == 140
assert captured["mu"] == 150
assert captured["lambda"] == 150
assert captured["mu"] == 300
assert captured["lambda"] == 300
def test_small_population_scales_warm_and_educated_seed_families(config_eos: ConfigEOS):
_configure_hourly_grid(config_eos)
config_eos.optimization.genetic.individuals = 100
opt = GeneticOptimization(fixed_seed=42)
opt.optimize_ev = False
opt.setup_deap_environment({"home_appliance": 0}, start_hour=0)
start_solution = [5] * opt.total_slots
captured: dict[str, object] = {}
def warm_neighbors(_solution, count):
captured["warm_count"] = count
return [[6] * opt.total_slots for _ in range(count)]
def educated(count):
captured["educated_count"] = count
return [[7] * opt.total_slots for _ in range(count)]
def fake_ea(population, toolbox, **kwargs):
captured["population"] = list(population)
captured["mu"] = kwargs["mu"]
captured["lambda"] = kwargs["lambda_"]
for individual in population:
individual.fitness.values = (float(sum(individual)),)
individual.extra_data = (0.0, 0.0, 0.0)
kwargs["halloffame"].update(population)
return population, SimpleNamespace(select=lambda _name: [])
with (
patch.object(opt, "_mutated_warm_start_neighbors", side_effect=warm_neighbors),
patch.object(opt, "_educated_guess_individuals", side_effect=educated),
patch.object(
opt.toolbox,
"population",
side_effect=lambda n: [creator.Individual([9] * opt.total_slots) for _ in range(n)],
),
patch("akkudoktoreos.optimization.genetic.genetic.algorithms.eaMuPlusLambda", fake_ea),
):
opt.optimize(start_solution=start_solution, ngen=1)
population = captured["population"]
first_genes = [individual[0] for individual in population] # type: ignore[union-attr]
assert len(population) == 100 # type: ignore[arg-type]
assert first_genes.count(5) == 10
assert first_genes.count(6) == 20
assert first_genes.count(7) == 40
assert first_genes.count(9) == 30
assert captured["warm_count"] == 20
assert captured["educated_count"] == 40
assert captured["mu"] == 100
assert captured["lambda"] == 100
def test_local_search_moves_weak_export_to_later_expensive_import(config_eos: ConfigEOS):
_configure_hourly_grid(config_eos)
opt = GeneticOptimization(fixed_seed=42)
opt.optimize_ev = False
opt.optimize_dc_charge = True
opt.optimize_battery_grid_export = True
opt.bat_possible_charge_values = [1.0]
opt.setup_deap_environment({"home_appliance": 0}, start_hour=0)
slots = opt.total_slots
export_state = 5
self_consumption_state = 6
discharge_state = 1
source = 10
targets = list(range(20, 32))
base = [self_consumption_state] * slots
base[source] = export_state
for slot in targets:
base[slot] = 0
opt.simulation.elect_price_hourly = np.full(slots, 0.10)
opt.simulation.elect_price_hourly[targets] = 0.30
opt.simulation.elect_revenue_per_hour_arr = np.full(slots, 0.05)
opt.simulation.elect_revenue_per_hour_arr[source] = 0.20
opt.simulation.pv_prediction_wh = np.zeros(slots)
opt.simulation.load_energy_array = np.full(slots, 100.0)
def evaluate(individual):
export_value = -0.20 if individual[source] == export_state else 0.0
avoided_import = -0.05 * sum(individual[slot] == discharge_state for slot in targets)
return (export_value + avoided_import,)
opt.toolbox.register("evaluate", evaluate)
incumbent = creator.Individual(base)
incumbent.fitness.values = evaluate(incumbent)
best, evaluations, improvements, initial, final = opt._locally_improve_grid_export(
incumbent,
max_evaluations=96,
)
assert evaluations > 0
assert improvements == 1
assert final < initial
assert best[source] == self_consumption_state
assert sum(best[slot] == discharge_state for slot in targets) >= 6
def test_educated_guesses_encode_high_price_direct_marketing(config_eos: ConfigEOS):
+68 -68
View File
@@ -110,16 +110,16 @@
0,
0,
0,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
1,
1,
0,
@@ -237,12 +237,11 @@
0.0,
0.0,
0.0,
0.0,
0.21367321450420126,
0.022582049506752234,
0.3039575,
0.19320652266312205,
0.1358062627100041,
0.0692592282596561,
0.023370695807696434,
0.0,
0.0,
0.0,
@@ -262,7 +261,8 @@
0.0,
0.0,
0.0,
0.11745809857246167,
0.0,
0.09514375330616998,
0.15236390731688437,
0.10316291465819699,
0.05435777788576338,
@@ -272,10 +272,10 @@
0.0,
0.0
],
"Gesamt_Verluste": 3638.132237848992,
"Gesamtbilanz_Euro": 0.5580695656866563,
"Gesamteinnahmen_Euro": 1.0626586223779864,
"Gesamtkosten_Euro": 1.6207281880646427,
"Gesamt_Verluste": 3421.6165248449383,
"Gesamtbilanz_Euro": 0.5951993628823129,
"Gesamteinnahmen_Euro": 1.1298399163065491,
"Gesamtkosten_Euro": 1.725039279188862,
"Home_appliance_wh_per_hour": [
0.0,
0.0,
@@ -318,16 +318,16 @@
],
"home_appliance_energy_wh": {},
"Kosten_Euro_pro_Stunde": [
0.0,
0.0,
0.10013413727316416,
0.09007999454232955,
0.11436917344552285,
0.0,
0.07557452231671152,
0.0,
4.55656845588237e-17,
0.001414013162203277,
0.005881449073870462,
0.05258762370598476,
0.1614775630079859,
0.0,
0.0,
0.0,
0.26650619799999997,
@@ -358,16 +358,16 @@
0.16484566
],
"Netzbezug_Wh_pro_Stunde": [
0.0,
0.0,
439.1848126015972,
407.23324838304495,
546.436566868241,
0.0,
402.20607938643707,
0.0,
2.2737367544323206e-13,
6.433180901743754,
25.909467285772962,
175.4675465665157,
505.40708296709204,
0.0,
0.0,
0.0,
912.38,
@@ -402,12 +402,11 @@
0.0,
0.0,
0.0,
0.0,
3052.474492917161,
322.60070723931767,
4342.25,
2760.093180901744,
1940.089467285773,
989.4175465665157,
333.86708296709196,
0.0,
0.0,
0.0,
@@ -427,7 +426,8 @@
0.0,
0.0,
0.0,
1677.9728367494527,
0.0,
1359.1964758024283,
2176.6272473840627,
1473.7559236885286,
776.5396840823341,
@@ -438,16 +438,16 @@
0.0
],
"Verluste_Pro_Stunde": [
97.85621559422765,
101.92059640365329,
37.96737751219166,
46.38878980596536,
39.91398802418894,
106.67021307906845,
582.6179999999995,
154.77306084994075,
51.82392952637247,
543.9059151312817,
0.0,
0.0,
0.0,
0.0,
108.98319763338179,
106.80230977350088,
133.7321802766326,
0.0,
@@ -467,7 +467,7 @@
165.15986945297027,
65.92300791736113,
538.2984000000001,
240.58122702042965,
278.8343903340726,
0.0,
0.0,
0.0,
@@ -479,35 +479,35 @@
],
"akku_soc_pro_stunde": [
80.0,
79.16421888032488,
78.69989843940196,
79.80862032896276,
79.51691497639054,
95.70074830972388,
81.05464937533866,
82.3432268699488,
83.45194875950959,
84.8915023574644,
100.0,
100.0,
100.0,
100.0,
100.0,
96.82533215994886,
92.60407390132525,
92.60407390132525,
92.60407390132525,
90.38140689030597,
86.64496577460349,
83.65624332281286,
81.03576295091202,
78.64117789581559,
76.53679600876325,
74.35484869471367,
70.91195845376538,
67.41428833522274,
68.03406859993031,
69.53927969874645,
71.94548598914552,
76.53326014061692,
78.36445480498806,
93.3171881383214,
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,
100.0,
100.0,
100.0,
@@ -717,18 +717,18 @@
0.0,
0.0,
0.0,
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,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
+284 -284
View File
@@ -15,7 +15,7 @@
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
@@ -110,23 +110,23 @@
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1,
1,
1,
0,
0,
1,
1,
1,
0,
1,
1,
1,
@@ -145,7 +145,7 @@
0,
1,
1,
1,
0,
0,
0
],
@@ -161,16 +161,16 @@
0.0,
0.0,
0.0,
0.875,
0.625,
0.625,
0.375,
0.875,
0.75,
0.75,
0.375,
0.1,
1.0,
0.0,
0.375,
0.625,
1.0,
0.625,
0.875,
0.0,
0.3,
0.0,
0.0,
0.0,
@@ -202,16 +202,16 @@
],
"result": {
"Last_Wh_pro_Stunde": [
10713.07,
7963.91,
8220.56,
7772.03,
13323.67,
9456.82,
9496.22,
5243.78,
2233.12,
1178.71,
7953.07,
12103.91,
1320.56,
5272.03,
8063.67,
17059.173961709643,
8116.22,
10763.78,
1129.12,
4490.71,
1050.98,
988.56,
912.38,
@@ -231,8 +231,8 @@
827.01,
1257.98,
1232.67,
871.26,
860.88,
3371.26,
3360.88,
1158.03,
1222.72,
1221.04,
@@ -243,43 +243,43 @@
],
"EAuto_SoC_pro_Stunde": [
5.0,
20.294999999999998,
31.22,
42.144999999999996,
48.699999999999996,
63.995000000000005,
77.105,
90.215,
96.77,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518,
98.518
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
],
"Einnahmen_Euro_pro_Stunde": [
0.0,
@@ -313,19 +313,45 @@
0.0,
0.0,
0.0,
0.053661230306193436,
0.05435777788576338,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
],
"Gesamt_Verluste": 6227.580163897914,
"Gesamtbilanz_Euro": 12.033027623527284,
"Gesamteinnahmen_Euro": 0.10801900819195681,
"Gesamtkosten_Euro": 12.14104663171924,
"Gesamt_Verluste": 8756.080717524794,
"Gesamtbilanz_Euro": 9.985237573040141,
"Gesamteinnahmen_Euro": 0.0,
"Gesamtkosten_Euro": 9.985237573040141,
"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,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
@@ -337,36 +363,36 @@
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,
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,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
@@ -378,112 +404,86 @@
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,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0
]
},
"Kosten_Euro_pro_Stunde": [
2.23047612,
1.5308798733801507,
1.4889564723943407,
1.242149738556099,
1.3742255912165289,
0.8479283355019763,
1.2336553365360492,
0.5407163922683618,
0.20558284318867343,
0.1614775630079859,
0.5980075076051746,
1.473337992,
0.0,
0.0,
0.0,
2.3442290999999997,
0.9428514400845971,
1.762926136273946,
0.05258762370598476,
0.0,
0.0,
0.0,
0.26650619799999997,
0.19588158,
0.0,
0.0,
0.22802125600000003,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.08281196422730584,
0.16677339,
0.264113778992023,
0.2536463762855701,
0.1306329312971816,
0.07362195915902499,
0.060401289430882174,
0.009619897970888898,
0.07029121023060134,
4.179128154646605e-17,
2.3325608707865465e-05,
0.0021886029750169123,
0.04064749825228731,
0.1994538035279033,
0.013012984677295973,
0.08357424731947552,
0.0,
0.0,
0.0,
0.293043269,
0.214398479,
0.16484566
],
"Netzbezug_Wh_pro_Stunde": [
9782.789999999999,
6920.795087613701,
7113.9821901306295,
6610.695787951565,
7476.74423948057,
4231.179318872138,
5612.626644840988,
2382.0105386271443,
685.962106068313,
505.40708296709204,
2622.8399456367306,
6660.66,
0.0,
0.0,
0.0,
11697.75,
4289.587989465865,
7766.194432924872,
175.4675465665157,
0.0,
0.0,
0.0,
912.38,
704.61,
0.0,
0.0,
694.34,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
248.38621543882974,
506.91,
799.8600211751151,
833.8145177040436,
537.58407941227,
322.9033296448464,
273.0618871197205,
45.96224544141853,
374.088399311343,
2.2737367544323206e-13,
0.11639525303326081,
9.957247384062384,
202.83182760622412,
907.4331370696236,
57.32592368852852,
278.85968408233407,
0.0,
0.0,
0.0,
987.01,
733.99,
592.97
],
@@ -519,8 +519,8 @@
0.0,
0.0,
0.0,
766.589004374192,
776.5396840823341,
0.0,
0.0,
0.0,
0.0,
0.0,
@@ -528,84 +528,84 @@
0.0
],
"Verluste_Pro_Stunde": [
483.0,
345.0162105136441,
345.0194628156755,
207.04269455418785,
503.6273087376684,
449.2115182646565,
424.3543973809186,
225.74286463525735,
102.70945272819762,
40.06404995605101,
945.0059934764078,
1152.0,
114.42806532440363,
768.1700560862765,
752.877211603942,
1152.0,
362.1897587359037,
485.44493195098454,
118.73010558798194,
641.287728412984,
106.80230977350088,
133.7321802766326,
0.0,
0.0,
70.41409090909087,
118.37045454545455,
94.68272727272722,
0.0,
83.01681818181817,
75.86045454545456,
66.66681818181814,
69.12409090909085,
109.07302361034766,
116.99491129525349,
32.79597062197777,
0.0,
0.0012025410138062752,
3.292931608338464,
22.312089529472388,
54.18759955738153,
86.6234264543665,
165.15986945297027,
65.92300791736113,
538.2984000000001,
441.9379674303641,
261.1952696860876,
84.86003031772043,
0.0,
166.26381931274682,
68.89237644835487,
176.85071084262336,
93.18476208988011,
111.78035844493081,
90.18403329253945,
134.59227272727276,
0.0,
0.0,
0.0
],
"akku_soc_pro_stunde": [
80.0,
80.0,
80.00045029204567,
80.00099092581443,
80.00217688565297,
80.57515768392155,
81.55325541349534,
81.8408775629653,
82.36151269172245,
83.68121971195016,
84.79410998850713,
81.619442148456,
77.39818388983241,
77.39818388983241,
77.39818388983241,
75.17551687881311,
71.43907576311062,
68.45035331131999,
65.82987293941916,
63.43528788432273,
61.33090599727039,
59.14895868322081,
55.706068442272525,
52.208398323729895,
52.82817858843747,
54.33338968725362,
56.73959597765268,
61.327370129124084,
63.158564793495216,
78.11129812682856,
90.387352777672,
97.64277693561888,
100.0,
100.0,
98.60068046043587,
96.11252124341868,
91.86402778611841,
91.86402778611841
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
],
"Electricity_price": [
0.000228,
@@ -702,16 +702,16 @@
0.0,
0.0,
0.0,
0.875,
0.625,
0.625,
0.375,
0.875,
0.75,
0.75,
0.375,
0.1,
1.0,
0.0,
0.375,
0.625,
1.0,
0.625,
0.875,
0.0,
0.3,
0.0,
0.0,
0.0,
@@ -795,37 +795,37 @@
"capacity_wh": 60000,
"charging_efficiency": 0.95,
"max_charge_power_w": 11040,
"soc_wh": 59110.8,
"soc_wh": 59897.4,
"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,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
1.0,
1.0,
0.0,
0.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
@@ -844,63 +844,63 @@
0.0,
1.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,
3.0,
1.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,
0.0,
2.0,
2.0,
4.0,
5.0,
6.0,
0.0,
1.0,
2.0,
0.0,
5.0,
3.0,
3.0,
1.0,
5.0,
4.0,
4.0,
1.0,
1.0,
2.0,
2.0,
1.0,
4.0,
6.0,
4.0,
5.0,
6.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,
3.0
2.0,
29.0
],
"washingstart": 13,
"washingstart": 39,
"appliance_starts": {
"dishwasher1": [
"2025-01-15 13:00:00+01:00"
"2025-01-16 15:00:00+01:00"
]
}
}
+236 -236
View File
@@ -15,7 +15,7 @@
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
@@ -112,6 +112,29 @@
0,
1,
1,
1,
1,
1,
0,
0,
0,
0,
1,
1,
1,
0,
0,
1,
1,
0,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
0,
@@ -124,29 +147,6 @@
1,
0,
0,
1,
1,
1,
1,
1,
1,
1,
1,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
1,
0,
1,
1,
0,
0
],
"battery_grid_export_allowed": [],
@@ -161,16 +161,16 @@
0.0,
0.0,
0.0,
0.625,
1.0,
0.5,
0.75,
0.0,
0.375,
0.625,
1.0,
0.625,
0.875,
0.5,
0.375,
1.0,
0.375,
0.0,
0.0,
0.3,
0.0,
0.0,
0.0,
@@ -202,16 +202,16 @@
],
"result": {
"Last_Wh_pro_Stunde": [
12093.07,
6583.91,
9600.56,
10792.03,
6683.67,
5316.82,
12256.22,
5243.78,
7953.07,
12103.91,
1320.56,
5272.03,
8063.67,
17059.173961709643,
8116.22,
10763.78,
1129.12,
1178.71,
4490.71,
1050.98,
988.56,
912.38,
@@ -229,10 +229,10 @@
992.46,
1155.99,
827.01,
3757.98,
3732.67,
871.26,
860.88,
1257.98,
1232.67,
3371.26,
3360.88,
1158.03,
1222.72,
1221.04,
@@ -243,43 +243,43 @@
],
"EAuto_SoC_pro_Stunde": [
5.0,
22.48,
31.22,
44.330000000000005,
59.62499999999999,
15.925,
33.405,
33.405,
39.96,
50.885000000000005,
68.365,
74.92,
92.4,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955,
98.955
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
],
"Einnahmen_Euro_pro_Stunde": [
0.0,
@@ -321,10 +321,10 @@
0.0,
0.0
],
"Gesamt_Verluste": 7415.050669156861,
"Gesamtbilanz_Euro": 10.086958235191952,
"Gesamt_Verluste": 8756.080717524794,
"Gesamtbilanz_Euro": 9.985237573040141,
"Gesamteinnahmen_Euro": 0.0,
"Gesamtkosten_Euro": 10.086958235191952,
"Gesamtkosten_Euro": 9.985237573040141,
"Home_appliance_wh_per_hour": [
0.0,
0.0,
@@ -353,10 +353,10 @@
0.0,
0.0,
0.0,
2500.0,
2500.0,
0.0,
0.0,
2500.0,
2500.0,
0.0,
0.0,
0.0,
@@ -394,10 +394,10 @@
0.0,
0.0,
0.0,
2500.0,
2500.0,
0.0,
0.0,
2500.0,
2500.0,
0.0,
0.0,
0.0,
@@ -408,82 +408,82 @@
]
},
"Kosten_Euro_pro_Stunde": [
1.5419161199999998,
0.2524105556335792,
1.7777665549410084,
1.809540886,
0.24971825220475874,
0.12061259291950001,
1.83015129135275,
0.5407163922683618,
0.5980075076051746,
1.473337992,
0.0,
0.0,
0.0,
2.3442290999999997,
0.9428514400845971,
1.762926136273946,
0.05258762370598476,
0.1614775630079859,
0.0,
0.0,
0.0,
0.26650619799999997,
0.19588158,
0.0,
0.0,
0.22802125600000003,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.08281196422730584,
0.16677339,
0.264113778992023,
0.2536463762855701,
0.1306329312971816,
0.07362195915902499,
0.060401289430882174,
0.009619897970888898,
0.4410873661898387,
0.0,
2.3325608707865465e-05,
0.0021886029750169123,
0.07029121023060134,
4.179128154646605e-17,
0.04064749825228731,
0.1994538035279033,
0.013012984677295973,
0.0,
0.17784012884918773,
0.08357424731947552,
0.0,
0.0,
0.293043269,
0.214398479,
0.16484566
],
"Netzbezug_Wh_pro_Stunde": [
6762.789999999999,
1141.0965444556023,
8493.867916583891,
9630.34,
1358.64119806724,
601.8592461052895,
8326.43899614536,
2382.0105386271443,
2622.8399456367306,
6660.66,
0.0,
0.0,
0.0,
11697.75,
4289.587989465865,
7766.194432924872,
175.4675465665157,
505.40708296709204,
0.0,
0.0,
0.0,
912.38,
704.61,
0.0,
0.0,
694.34,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
248.38621543882974,
506.91,
799.8600211751151,
833.8145177040436,
537.58407941227,
322.9033296448464,
273.0618871197205,
45.96224544141853,
2347.45804252176,
0.0,
0.11639525303326081,
9.957247384062384,
374.088399311343,
2.2737367544323206e-13,
202.83182760622412,
907.4331370696236,
57.32592368852852,
0.0,
556.6201215937018,
278.85968408233407,
0.0,
0.0,
987.01,
733.99,
592.97
],
@@ -528,84 +528,84 @@
0.0
],
"Verluste_Pro_Stunde": [
945.0059934764078,
1152.0,
876.0523853346722,
414.00574999006693,
483.0,
359.25494376806876,
303.4931095326348,
556.8118795374434,
225.74286463525735,
114.42806532440363,
768.1700560862765,
752.877211603942,
1152.0,
362.1897587359037,
485.44493195098454,
118.73010558798194,
40.06404995605101,
641.287728412984,
106.80230977350088,
133.7321802766326,
0.0,
0.0,
70.41409090909087,
118.37045454545455,
94.68272727272722,
0.0,
83.01681818181817,
75.86045454545456,
66.66681818181814,
69.12409090909085,
109.07302361034766,
116.99491129525349,
32.79597062197777,
0.0,
0.0012025410138062752,
3.292931608338464,
22.312089529472388,
54.18759955738153,
86.6234264543665,
165.15986945297027,
2.727365102611202,
238.2983999999999,
441.9379674303641,
261.1952696860876,
65.92300791736113,
538.2984000000001,
166.26381931274682,
68.89237644835487,
176.85071084262336,
131.21108264656203,
35.877614591244196,
93.18476208988011,
111.78035844493081,
90.18403329253945,
134.59227272727276,
0.0,
0.0,
0.0
],
"akku_soc_pro_stunde": [
80.0,
61.06060606060606,
42.12266726939745,
42.122826991343764,
42.122826991343764,
44.435464318234565,
47.11582847191886,
47.249491792403404,
47.770126921160546,
51.0681854097156,
52.181075686272585,
49.006407846221435,
44.78514958759783,
44.78514958759783,
44.78514958759783,
42.562482576578546,
38.82604146087607,
35.837319009085434,
33.2168386371846,
30.822253582088187,
28.717871695035846,
26.53592438098626,
23.09303414003797,
19.595364021495346,
20.215144286202914,
21.72035538501907,
24.126561675418138,
28.71433582688953,
28.79009596862873,
35.40949596862873,
47.68555061947217,
54.94097477741905,
59.85349452304748,
61.241634361507465,
62.2382347668198,
59.7500755498026,
55.50158209250233,
55.50158209250233
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
],
"Electricity_price": [
0.000228,
@@ -702,16 +702,16 @@
0.0,
0.0,
0.0,
0.625,
1.0,
0.5,
0.75,
0.0,
0.375,
0.625,
1.0,
0.625,
0.875,
0.5,
0.375,
1.0,
0.375,
0.0,
0.0,
0.3,
0.0,
0.0,
0.0,
@@ -795,43 +795,44 @@
"capacity_wh": 60000,
"charging_efficiency": 0.95,
"max_charge_power_w": 11040,
"soc_wh": 59373.0,
"soc_wh": 59897.4,
"initial_soc_percentage": 5
},
"start_solution": [
1.0,
0.0,
2.0,
2.0,
0.0,
1.0,
2.0,
2.0,
1.0,
2.0,
1.0,
1.0,
0.0,
0.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,
1.0,
1.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,
@@ -842,65 +843,64 @@
0.0,
0.0,
1.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
0.0,
0.0,
0.0,
2.0,
3.0,
6.0,
1.0,
4.0,
3.0,
1.0,
6.0,
0.0,
1.0,
3.0,
6.0,
2.0,
4.0,
3.0,
5.0,
2.0,
1.0,
6.0,
1.0,
3.0,
4.0,
3.0,
3.0,
0.0,
2.0,
3.0,
0.0,
0.0,
6.0,
4.0,
5.0,
6.0,
1.0,
2.0,
4.0,
3.0,
3.0,
0.0,
6.0,
5.0,
4.0,
1.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,
0.0,
2.0,
2.0,
4.0,
3.0,
1.0,
6.0,
1.0,
4.0,
27.0
5.0,
5.0,
3.0,
0.0,
2.0,
29.0
],
"washingstart": 37,
"washingstart": 39,
"appliance_starts": {
"dishwasher1": [
"2025-01-16 13:00:00+01:00"
"2025-01-16 15:00:00+01:00"
]
}
}