-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsimulation.py
More file actions
444 lines (398 loc) · 24.4 KB
/
Copy pathsimulation.py
File metadata and controls
444 lines (398 loc) · 24.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
# minesimulator/simulation.py
import numpy as np
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Tuple, Deque
import time as py_time
import traceback
from collections import deque
import math
# --- Import necessary constants, states, and functions from utils ---
from utils import (
clamp, DEFAULT_BATTERY_CAPACITY_KWH, DEFAULT_MAX_SOLAR_KW,
DEFAULT_DAY_LENGTH_SECONDS, DEFAULT_NUM_MINERS,
DEFAULT_CONTROLLER_TYPE,
INITIAL_BATTERY_HEALTH, WEAR_FACTOR_PER_CYCLE, WEAR_FACTOR_PER_HOUR,
MINER_STATUS_OFF, MINER_STATUS_RUNNING, MINER_STATUS_VOLT_LIMITED, MINER_STATUS_PWR_LIMITED,
ADVANCED_SOLAR_TREND_WINDOW_SECONDS, ADVANCED_CONTROLLER_TICK_RATE_SECONDS,
ADVANCED_SOC_TARGET_HIGH, ADVANCED_MIN_BATT_DISCHARGE_SOC, ADVANCED_SOC_TARGET_LOW,
LOW_VOLTAGE_SHUTDOWN_V, BATTERY_NOMINAL_VOLTAGE, BATTERY_VOLTAGE_RANGE,
simulate_voltage,
MINER_NOMINAL_POWER_KW, MINER_MIN_POWER_KW, MINER_MAX_POWER_KW, MINER_OFF_POWER_KW,
BASE_LOAD_INVERTER_NETWORK_KW, BASE_LOAD_FAN_KW_PER_MINER,
get_hashes_per_joule, MINER_POWER_CHANGE_COOLDOWN_SECONDS,
_calculate_hashrate
)
# --- Define the fixed internal simulation step size ---
INTERNAL_SIM_STEP_SECONDS = 1.0 # Simulate physics at 1-second granularity
@dataclass
class MinerState:
"""Represents the state of a single mining unit."""
id: int
requested_power_limit_kw: float = MINER_OFF_POWER_KW
actual_power_limit_kw: float = MINER_OFF_POWER_KW
status: str = MINER_STATUS_OFF
current_hashes_per_joule: float = 0.0
current_hashrate_mh_s: float = 0.0
last_power_limit_change_time: float = -1.0
total_time_on_seconds: float = 0.0
def update_derived_stats(self):
"""Calculates status, efficiency, and hashrate."""
if self.actual_power_limit_kw <= MINER_OFF_POWER_KW:
self.status = MINER_STATUS_OFF
elif self.status not in [MINER_STATUS_VOLT_LIMITED, MINER_STATUS_PWR_LIMITED]:
self.status = MINER_STATUS_RUNNING
if self.status == MINER_STATUS_OFF:
self.current_hashes_per_joule = 0.0
self.current_hashrate_mh_s = 0.0
else:
self.current_hashes_per_joule = get_hashes_per_joule(self.actual_power_limit_kw)
self.current_hashrate_mh_s = _calculate_hashrate(self.actual_power_limit_kw)
@dataclass
class SimulationState:
"""Represents the overall state of the simulation."""
time_seconds: float = 0.0
# delta_time now represents the fixed internal step size
delta_time: float = INTERNAL_SIM_STEP_SECONDS
# Solar
max_solar_kw: float = DEFAULT_MAX_SOLAR_KW
day_length_seconds: float = DEFAULT_DAY_LENGTH_SECONDS
cloud_cover: float = 0.0
current_solar_kw: float = 0.0
# Adjust solar_history maxlen based on internal step size
solar_history: Deque[Tuple[float, float]] = field(
default_factory=lambda: deque(maxlen=max(10, int(ADVANCED_SOLAR_TREND_WINDOW_SECONDS / INTERNAL_SIM_STEP_SECONDS * 2)))
)
# Battery (fields unchanged)
battery_capacity_kwh: float = DEFAULT_BATTERY_CAPACITY_KWH
battery_base_capacity_kwh: float = DEFAULT_BATTERY_CAPACITY_KWH
battery_kwh: float = field(default=DEFAULT_BATTERY_CAPACITY_KWH * ADVANCED_SOC_TARGET_HIGH)
battery_health: float = INITIAL_BATTERY_HEALTH
battery_charge_power_limit_kw: float = 50.0
battery_discharge_power_limit_kw: float = 50.0
battery_voltage: float = field(default=BATTERY_NOMINAL_VOLTAGE)
cumulative_charge_kwh: float = 0.0
cumulative_discharge_kwh: float = 0.0
battery_equivalent_cycles: float = 0.0
battery_power_flow_kw: float = 0.0
# Miners (fields unchanged)
miners: List[MinerState] = field(default_factory=list)
total_miner_requested_limit_kw: float = 0.0
total_miner_actual_limit_kw: float = 0.0
# Loads (fields unchanged)
current_base_load_kw: float = 0.0
total_system_load_kw: float = 0.0
# Uptime (field unchanged)
total_mine_on_time_seconds: float = 0.0
# System (fields unchanged)
controller_type: str = DEFAULT_CONTROLLER_TYPE
is_running: bool = False
last_controller_run_time: float = field(default=-ADVANCED_CONTROLLER_TICK_RATE_SECONDS)
def __post_init__(self):
"""Initialize state."""
if not self.miners:
num_miners = DEFAULT_NUM_MINERS
self.miners = [MinerState(id=i) for i in range(num_miners)]
self.battery_capacity_kwh = self.battery_base_capacity_kwh * self.battery_health
self.battery_kwh = clamp(self.battery_base_capacity_kwh * ADVANCED_SOC_TARGET_HIGH, 0, self.battery_capacity_kwh)
self.battery_voltage = simulate_voltage(self.get_soc())
self.current_base_load_kw = BASE_LOAD_INVERTER_NETWORK_KW
self.total_system_load_kw = self.current_base_load_kw
# Set delta_time explicitly here too
self.delta_time = INTERNAL_SIM_STEP_SECONDS
def get_soc(self) -> float:
"""Returns State of Charge (0.0 to 1.0)."""
if self.battery_capacity_kwh <= 1e-6: return 0.0
current_kwh = clamp(self.battery_kwh, 0, self.battery_capacity_kwh)
soc = current_kwh / self.battery_capacity_kwh
return clamp(soc, 0.0, 1.0)
def update_num_miners(self, num_miners: int):
"""Adds or removes miners."""
num_miners = max(0, num_miners)
current_count = len(self.miners)
if num_miners > current_count:
for i in range(current_count, num_miners): self.miners.append(MinerState(id=i))
elif num_miners < current_count:
self.miners = self.miners[:num_miners]
def reset_state(self):
"""Resets the simulation state fields."""
print("Resetting SimulationState fields...")
self.time_seconds = 0.0
self.delta_time = INTERNAL_SIM_STEP_SECONDS # Use fixed step
self.max_solar_kw = DEFAULT_MAX_SOLAR_KW
self.day_length_seconds = DEFAULT_DAY_LENGTH_SECONDS
self.cloud_cover = 0.0
self.current_solar_kw = 0.0
self.solar_history.clear()
self.battery_base_capacity_kwh = DEFAULT_BATTERY_CAPACITY_KWH
self.battery_health = INITIAL_BATTERY_HEALTH
self.battery_capacity_kwh = self.battery_base_capacity_kwh * self.battery_health
self.battery_kwh = self.battery_capacity_kwh * ADVANCED_SOC_TARGET_HIGH
self.battery_voltage = simulate_voltage(self.get_soc())
self.battery_charge_power_limit_kw = 50.0
self.battery_discharge_power_limit_kw = 50.0
self.cumulative_charge_kwh = 0.0
self.cumulative_discharge_kwh = 0.0
self.battery_equivalent_cycles = 0.0
self.battery_power_flow_kw = 0.0
num_miners = DEFAULT_NUM_MINERS
self.miners = [MinerState(id=i) for i in range(num_miners)]
self.total_miner_requested_limit_kw = 0.0
self.total_miner_actual_limit_kw = 0.0
self.current_base_load_kw = BASE_LOAD_INVERTER_NETWORK_KW
self.total_system_load_kw = self.current_base_load_kw
self.total_mine_on_time_seconds = 0.0
self.controller_type = DEFAULT_CONTROLLER_TYPE
self.last_controller_run_time = -ADVANCED_CONTROLLER_TICK_RATE_SECONDS
self.is_running = False
print("SimulationState fields reset complete.")
def get_solar_trend_kw_per_minute(self) -> float:
"""Calculates solar power trend."""
if len(self.solar_history) < 2: return 0.0
try:
times = np.array([t for t, p in self.solar_history])
powers = np.array([p for t, p in self.solar_history])
time_diff = times[-1] - times[0]
if time_diff < INTERNAL_SIM_STEP_SECONDS: return 0.0 # Need difference >= step size
coeffs = np.polyfit(times, powers, 1)
return coeffs[0] * 60.0
except (np.linalg.LinAlgError, ValueError, IndexError) as e:
# Fallback calculation... (unchanged)
print(f"[Warning] Solar trend calculation using polyfit failed: {e}. Using fallback.")
try:
relevant_points = list(self.solar_history)
if len(relevant_points) < 2: return 0.0
first_time, first_power = relevant_points[0]
last_time, last_power = relevant_points[-1]
time_diff_seconds = last_time - first_time
if time_diff_seconds <= 1e-6: return 0.0
power_diff_kw = last_power - first_power
return (power_diff_kw / time_diff_seconds) * 60.0
except Exception as fallback_e:
print(f"[Error] Solar trend fallback calculation also failed: {fallback_e}")
return 0.0
class Simulator:
"""Manages the simulation loop and state updates using a fixed internal step time."""
def __init__(self, initial_state: SimulationState = None):
self.state = initial_state if initial_state else SimulationState()
# Speed multiplier is removed from here, will be handled by GUI loop
# REMOVED: set_simulation_speed method
# REMOVED: _calculate_delta_time method
def update_solar(self):
"""Calculates current solar power."""
# Uses self.state.time_seconds, self.state.day_length_seconds, self.state.max_solar_kw
# Logic remains the same, but assumes calculations are for the current time step start
if self.state.day_length_seconds <= 0:
self.state.current_solar_kw = 0.0
self.state.solar_history.append((self.state.time_seconds, 0.0))
return
angle = (2 * np.pi * (self.state.time_seconds % self.state.day_length_seconds)) / self.state.day_length_seconds
base_solar = self.state.max_solar_kw * max(0, np.sin(angle))
cloud_adjusted_solar = base_solar * (1.0 - self.state.cloud_cover)
noise_std_dev_factor = 0.02
current_output_factor = max(0, np.sin(angle))
noise_std_dev = self.state.max_solar_kw * noise_std_dev_factor * current_output_factor
noise = np.random.normal(0, noise_std_dev) if noise_std_dev > 0 else 0
self.state.current_solar_kw = max(0, cloud_adjusted_solar + noise)
self.state.solar_history.append((self.state.time_seconds, self.state.current_solar_kw))
def update_battery(self):
"""Updates battery charge/voltage based on net power flow using fixed internal step time."""
dt = self.state.delta_time # Now fixed (e.g., 1.0s)
if dt <= 0:
self.state.battery_power_flow_kw = 0.0
return 0.0
dt_hours = dt / 3600.0
net_power_kw = self.state.current_solar_kw - self.state.total_system_load_kw
delta_kwh = 0.0
# Charging/Discharging logic remains the same, using the fixed dt_hours
if net_power_kw > 0: # Charging
charge_power = min(net_power_kw, self.state.battery_charge_power_limit_kw)
potential_charge_kwh = charge_power * dt_hours
max_charge_kwh = max(0, self.state.battery_capacity_kwh - self.state.battery_kwh)
delta_kwh = min(potential_charge_kwh, max_charge_kwh)
self.state.cumulative_charge_kwh += delta_kwh
elif net_power_kw < 0: # Discharging
needed_power = abs(net_power_kw)
discharge_power = min(needed_power, self.state.battery_discharge_power_limit_kw)
potential_discharge_kwh = discharge_power * dt_hours
min_kwh_limit = self.state.battery_capacity_kwh * ADVANCED_MIN_BATT_DISCHARGE_SOC
available_kwh_above_min = max(0, self.state.battery_kwh - min_kwh_limit)
actual_discharge_kwh = min(potential_discharge_kwh, available_kwh_above_min)
delta_kwh = -actual_discharge_kwh
self.state.cumulative_discharge_kwh += actual_discharge_kwh
# Apply energy change
self.state.battery_kwh += delta_kwh
self.state.battery_kwh = clamp(self.state.battery_kwh, 0, self.state.battery_capacity_kwh)
# Calculate and store power flow
if dt_hours > 1e-9: self.state.battery_power_flow_kw = delta_kwh / dt_hours
else: self.state.battery_power_flow_kw = 0.0
# Update voltage
self.state.battery_voltage = simulate_voltage(self.state.get_soc())
return delta_kwh
def apply_miner_power_requests(self, requested_limits: List[Tuple[int, float]]):
"""Determines final actual miner power limits and total system load."""
dt = self.state.delta_time # Use fixed step
sim_time = self.state.time_seconds
if dt <= 0: return
requests_dict = dict(requested_limits)
voltage_shutdown = self.state.battery_voltage < LOW_VOLTAGE_SHUTDOWN_V
# Calculate Base Load
num_active_miners_prev_step = sum(1 for m in self.state.miners if m.actual_power_limit_kw > MINER_OFF_POWER_KW)
fan_load = num_active_miners_prev_step * BASE_LOAD_FAN_KW_PER_MINER
self.state.current_base_load_kw = BASE_LOAD_INVERTER_NETWORK_KW + fan_load
# Determine Available Power for Miners
dischargeable_kwh = max(0, self.state.battery_kwh - (self.state.battery_capacity_kwh * ADVANCED_MIN_BATT_DISCHARGE_SOC))
max_power_from_battery_kw = 0
if dt > 0 and not voltage_shutdown:
sim_interval_hours = dt / 3600.0
max_power_from_energy = dischargeable_kwh / sim_interval_hours if sim_interval_hours > 0 else 0
max_power_from_battery_kw = min(self.state.battery_discharge_power_limit_kw, max_power_from_energy)
max_power_from_battery_kw = max(0, max_power_from_battery_kw)
total_grid_power_available = self.state.current_solar_kw + max_power_from_battery_kw
available_power_for_miners = max(0, total_grid_power_available - self.state.current_base_load_kw)
# Process Miner Requests & Constraints
self.state.total_miner_requested_limit_kw = 0.0
miners_power_targets = {}
# (Logic for processing requests, cooldown, voltage limits remains unchanged)
for miner in self.state.miners:
requested_kw = requests_dict.get(miner.id, MINER_OFF_POWER_KW)
requested_kw = clamp(requested_kw, MINER_OFF_POWER_KW, MINER_MAX_POWER_KW)
time_since_last_change = sim_time - miner.last_power_limit_change_time
is_in_cooldown = time_since_last_change < MINER_POWER_CHANGE_COOLDOWN_SECONDS
power_request_changed = abs(requested_kw - miner.requested_power_limit_kw) > 1e-6
if is_in_cooldown and power_request_changed: final_requested_kw = miner.requested_power_limit_kw
else:
final_requested_kw = requested_kw
if power_request_changed: miner.last_power_limit_change_time = sim_time
miner.requested_power_limit_kw = final_requested_kw
target_kw = final_requested_kw
miner_status = MINER_STATUS_RUNNING
if voltage_shutdown:
target_kw = MINER_OFF_POWER_KW
if final_requested_kw > MINER_OFF_POWER_KW: miner_status = MINER_STATUS_VOLT_LIMITED
if target_kw < MINER_MIN_POWER_KW: target_kw = MINER_OFF_POWER_KW
if target_kw == MINER_OFF_POWER_KW:
if miner_status != MINER_STATUS_VOLT_LIMITED: miner_status = MINER_STATUS_OFF
miners_power_targets[miner.id] = target_kw
self.state.total_miner_requested_limit_kw += target_kw
miner.status = miner_status
# Global Power Allocation
total_target_power = sum(miners_power_targets.values())
if total_target_power > available_power_for_miners:
scale_factor = available_power_for_miners / total_target_power if total_target_power > 0 else 0
scale_factor = clamp(scale_factor, 0.0, 1.0)
for miner in self.state.miners:
miner_id = miner.id
original_target = miners_power_targets[miner_id]
if original_target > MINER_OFF_POWER_KW:
scaled_kw = original_target * scale_factor
if scaled_kw < MINER_MIN_POWER_KW:
scaled_kw = MINER_OFF_POWER_KW
if miner.status != MINER_STATUS_VOLT_LIMITED: miner.status = MINER_STATUS_OFF
elif scale_factor < 1.0 and miner.status != MINER_STATUS_VOLT_LIMITED:
miner.status = MINER_STATUS_PWR_LIMITED
miners_power_targets[miner_id] = scaled_kw
# Finalize Actual Power, Uptime, Derived Stats
self.state.total_miner_actual_limit_kw = 0.0
any_miner_on_this_step = False
for miner in self.state.miners:
final_power_limit = miners_power_targets[miner.id]
if final_power_limit <= MINER_OFF_POWER_KW:
final_power_limit = MINER_OFF_POWER_KW
if miner.status != MINER_STATUS_VOLT_LIMITED: miner.status = MINER_STATUS_OFF
miner.actual_power_limit_kw = final_power_limit
self.state.total_miner_actual_limit_kw += final_power_limit
if miner.actual_power_limit_kw > MINER_OFF_POWER_KW:
miner.total_time_on_seconds += dt # Use fixed step dt
any_miner_on_this_step = True
miner.update_derived_stats()
if any_miner_on_this_step: self.state.total_mine_on_time_seconds += dt # Use fixed step dt
# Calculate Final Total System Load for this step
self.state.total_system_load_kw = self.state.total_miner_actual_limit_kw + self.state.current_base_load_kw
def update_battery_wear(self, energy_flow_kwh: float):
"""Applies battery capacity degradation."""
dt = self.state.delta_time # Use fixed step
dt_hours = dt / 3600.0
if dt <= 0 or self.state.battery_base_capacity_kwh <= 0: return
equivalent_cycles_this_step = abs(energy_flow_kwh) / (2 * self.state.battery_base_capacity_kwh)
self.state.battery_equivalent_cycles += equivalent_cycles_this_step
cycle_wear = equivalent_cycles_this_step * WEAR_FACTOR_PER_CYCLE
time_wear = WEAR_FACTOR_PER_HOUR * dt_hours
total_wear_this_step = cycle_wear + time_wear
self.state.battery_health = clamp(self.state.battery_health - total_wear_this_step, 0, 1.0)
self.state.battery_capacity_kwh = self.state.battery_base_capacity_kwh * self.state.battery_health
self.state.battery_kwh = clamp(self.state.battery_kwh, 0, self.state.battery_capacity_kwh)
def step(self, controller_decisions: List[Tuple[int, float]]):
"""
Performs ONE internal simulation step using fixed INTERNAL_SIM_STEP_SECONDS.
The is_running check is removed here; the GUI tick loop will control calling this.
"""
# dt is now fixed, defined in SimulationState as INTERNAL_SIM_STEP_SECONDS
dt = self.state.delta_time
# --- Simulation Logic Order ---
self.update_solar() # Calculates solar for the *start* of this small step
self.apply_miner_power_requests(controller_decisions) # Determines loads for this small step
actual_energy_flow_kwh = self.update_battery() # Updates battery based on net energy for this small step
self.update_battery_wear(actual_energy_flow_kwh) # Applies wear for this small step
self.state.time_seconds += dt # Advances time by the small fixed step
# Return the state *after* this single internal step
return self.state
# --- get_state_dict and load_state_from_dict remain the same ---
def get_state_dict(self) -> Dict:
"""Returns the simulation state as a serializable dictionary."""
state_dict = asdict(self.state, dict_factory=lambda x: {k: v for (k, v) in x if v is not None})
if isinstance(state_dict.get('solar_history'), deque):
state_dict['solar_history'] = list(state_dict['solar_history'])
if 'miners' in state_dict and isinstance(state_dict['miners'], list):
state_dict['miners'] = [asdict(miner) for miner in self.state.miners]
return state_dict
def load_state_from_dict(self, state_dict: Dict):
"""Loads simulation state from a dictionary."""
try:
# ... (loading logic remains the same, ensure delta_time is handled if present or set default) ...
loaded_is_running = state_dict.get('is_running', False)
state_dict['is_running'] = False
miner_dicts = state_dict.get('miners', [])
reconstructed_miners = []
valid_miner_keys = {f.name for f in MinerState.__dataclass_fields__.values()}
for md in miner_dicts:
md.setdefault('requested_power_limit_kw', MINER_OFF_POWER_KW); md.setdefault('actual_power_limit_kw', MINER_OFF_POWER_KW); md.setdefault('status', MINER_STATUS_OFF); md.setdefault('current_hashes_per_joule', 0.0); md.setdefault('current_hashrate_mh_s', 0.0); md.setdefault('last_power_limit_change_time', -1.0); md.setdefault('total_time_on_seconds', 0.0)
filtered_md = {k: v for k, v in md.items() if k in valid_miner_keys}
reconstructed_miners.append(MinerState(**filtered_md))
state_dict['miners'] = reconstructed_miners
state_dict.setdefault('time_seconds', 0.0)
# Ensure delta_time reflects the fixed internal step size upon load
state_dict['delta_time'] = INTERNAL_SIM_STEP_SECONDS
state_dict.setdefault('max_solar_kw', DEFAULT_MAX_SOLAR_KW); state_dict.setdefault('day_length_seconds', DEFAULT_DAY_LENGTH_SECONDS); state_dict.setdefault('cloud_cover', 0.0); state_dict.setdefault('current_solar_kw', 0.0)
solar_hist_list = state_dict.get('solar_history', [])
solar_hist_tuples = [tuple(item) if isinstance(item, list) else item for item in solar_hist_list if isinstance(item, (list, tuple)) and len(item) == 2]
solar_hist_maxlen = max(10, int(ADVANCED_SOLAR_TREND_WINDOW_SECONDS / INTERNAL_SIM_STEP_SECONDS * 2))
state_dict['solar_history'] = deque(solar_hist_tuples, maxlen=solar_hist_maxlen)
state_dict.setdefault('battery_base_capacity_kwh', DEFAULT_BATTERY_CAPACITY_KWH); state_dict.setdefault('battery_health', INITIAL_BATTERY_HEALTH)
base_cap = state_dict['battery_base_capacity_kwh']; health = state_dict['battery_health']
effective_capacity = base_cap * health; state_dict['battery_capacity_kwh'] = effective_capacity
default_kwh = effective_capacity * ADVANCED_SOC_TARGET_HIGH
state_dict['battery_kwh'] = clamp(state_dict.get('battery_kwh', default_kwh), 0, effective_capacity)
loaded_soc = state_dict['battery_kwh'] / effective_capacity if effective_capacity > 1e-6 else 0
state_dict.setdefault('battery_voltage', simulate_voltage(loaded_soc))
state_dict.setdefault('battery_charge_power_limit_kw', 50.0); state_dict.setdefault('battery_discharge_power_limit_kw', 50.0); state_dict.setdefault('cumulative_charge_kwh', 0.0); state_dict.setdefault('cumulative_discharge_kwh', 0.0); state_dict.setdefault('battery_equivalent_cycles', 0.0); state_dict.setdefault('battery_power_flow_kw', 0.0)
state_dict.setdefault('total_miner_requested_limit_kw', 0.0); state_dict.setdefault('total_miner_actual_limit_kw', 0.0); state_dict.setdefault('current_base_load_kw', BASE_LOAD_INVERTER_NETWORK_KW); state_dict.setdefault('total_system_load_kw', state_dict['current_base_load_kw']); state_dict.setdefault('total_mine_on_time_seconds', 0.0); state_dict.setdefault('controller_type', DEFAULT_CONTROLLER_TYPE); state_dict.setdefault('last_controller_run_time', -ADVANCED_CONTROLLER_TICK_RATE_SECONDS)
valid_state_keys = {f.name for f in SimulationState.__dataclass_fields__.values()}
filtered_state_dict = {k: v for k, v in state_dict.items() if k in valid_state_keys}
self.state = SimulationState(**filtered_state_dict)
self.state.miners = reconstructed_miners
for miner in self.state.miners: miner.update_derived_stats()
# self.last_real_time = py_time.monotonic() # Don't reset this here
print("Simulator state loaded successfully.")
except (TypeError, KeyError, ValueError) as e:
print(f"Error loading state into simulator: {e}")
traceback.print_exc()
raise ValueError("Failed to load state due to type/key mismatch or invalid value.") from e
except Exception as e:
print(f"Unexpected error loading state into simulator: {e}")
traceback.print_exc()
raise ValueError("Unexpected error during state loading.") from e
def reset_simulation(self):
"""Resets the simulator state object."""
self.state.reset_state()
# self.last_real_time = py_time.monotonic() # Don't reset here, GUI controls timing
print("Simulation reset complete.")