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 | @dataclass
class Capacitor:
"""
SimPy-driven capacitor with:
- charging (with leakage),
- self-discharge,
- load discharge with arbitrary current.
"""
env: simpy.Environment
id: int
params: CapacitorParams = field(default_factory=CapacitorParams)
keep_logs: bool = True
initial_voltage: float = 0.0 # V (initial charge state)
voltage_max: float = 2.0 # V (physical clamp / PMIC max)
# runtime state (initialized in __post_init__)
voltage: float = field(init=False, default=0.0)
internal_time: float = field(init=False, default=0.0) # seconds
energy: float = field(init=False, default=0.0) # Joule
voltage_source: float = field(init=False, default=0.0) # V (source equivalent)
current: float = field(init=False, default=0.0) # A (load)
voltage_history: List[Tuple[float, float]] = field(init=False, default_factory=list)
def __post_init__(self):
# Set initial voltage
self.voltage = min(self.initial_voltage, self.voltage_max)
self.energy = 0.5 * self.params.C * self.voltage * self.voltage
# start the SimPy process
self.action = self.env.process(self.run())
# ---------------- core helpers ----------------
def _tick_common(self):
self.internal_time = self.env.now / 1000.0 # ms -> s (your sim uses ms ticks)
# E = 1/2 C V^2
self.energy = 0.5 * self.params.C * self.voltage * self.voltage
if self.keep_logs:
self.voltage_history.append((self.internal_time, self.voltage))
# --------------- transitions ------------------
def capacitor_charging(self) -> float:
"""Charge toward voltage_source with leakage during one dt step."""
p = self.params
# explicit-Euler for source coupling + exact leakage decay over dt
# v_{t+dt} = v_t + dt*( (Vsrc - v_t)/(R_series*C) ) + v_t*(e^{-dt/(R_leak*C)} - 1)
self.voltage = (p.dt / (p.R_series * p.C)) * (
self.voltage_source - self.voltage
) + self.voltage * math.exp(-p.dt / (p.R_leakage * p.C))
self.voltage = min(max(self.voltage, 0.0), self.voltage_max)
self._tick_common()
return self.voltage
def capacitor_selfdischarge(self) -> float:
"""Leakage-only decay during one dt step."""
p = self.params
self.voltage *= math.exp(-p.dt / (p.R_leakage * p.C))
self.voltage = min(max(self.voltage, 0.0), self.voltage_max)
self._tick_common()
return self.voltage
def load_discharging(self, current: float) -> float:
"""
Discharge under an external load current for one dt step,
with leakage accounted.
"""
p = self.params
# energy drawn from cap by the load over dt
energy_consumed = self.voltage * current * p.dt # (approx) P*dt with P=V*I
self.energy = max(self.energy - energy_consumed, 0.0)
# ideal cap voltage drop from energy extraction:
# E = 1/2 C V^2 => V' = sqrt(max(V^2 - 2*E/C, 0))
v_after_ideal = math.sqrt(
max(self.voltage * self.voltage - (2.0 * energy_consumed) / p.C, 0.0)
)
# plus leakage during dt (exact decay applied to the *current* voltage):
# combine as in your original: v_next = v_after_ideal - v * (1 - e^{-dt/(R_leak*C)})
self.voltage = v_after_ideal - self.voltage * (
1.0 - math.exp(-p.dt / (p.R_leakage * p.C))
)
self.voltage = min(max(self.voltage, 0.0), self.voltage_max)
self._tick_common()
return self.voltage
def charge_and_discharge(self, load_current: float) -> float:
"""
Simultaneous charging from voltage_source and discharging to load.
Net power model: voltage changes based on (harvesting - consumption).
"""
p = self.params
# Energy harvested from source during dt (charging current through R_series)
# I_charge = (V_source - V_cap) / R_series
charge_current = (self.voltage_source - self.voltage) / p.R_series
charge_current = max(charge_current, 0.0) # can't push current back to source
energy_harvested = self.voltage_source * charge_current * p.dt
# Energy consumed by load during dt
energy_consumed = self.voltage * load_current * p.dt
# Net energy change
net_energy = energy_harvested - energy_consumed
# Update capacitor energy
self.energy = max(self.energy + net_energy, 0.0)
# Compute new voltage from energy: V = sqrt(2*E/C)
self.voltage = math.sqrt(2.0 * self.energy / p.C)
# Apply leakage decay
self.voltage *= math.exp(-p.dt / (p.R_leakage * p.C))
self.voltage = min(max(self.voltage, 0.0), self.voltage_max)
# keep stored energy consistent if we clipped
self.energy = 0.5 * p.C * self.voltage * self.voltage
self._tick_common()
return self.voltage
# ---------------- SimPy process ----------------
def run(self):
while True:
try:
# coarse idle scheduler; actual updates happen via interrupts below
yield self.env.timeout(10)
except simpy.Interrupt as it:
cause = it.cause
if cause == "capacitor_charging":
self.capacitor_charging()
elif cause == "capacitor_full":
self.capacitor_selfdischarge()
elif cause == "capacitor_discharge":
self.load_discharging(self.current)
elif cause == "capacitor_charge_and_discharge":
self.charge_and_discharge(self.current)
# ----------------------------
# convenience API
# ----------------------------
def charge_step(self, source_voltage: float):
"""One dt charging step (imperative helper; optionally call without interrupts)."""
self.voltage_source = source_voltage
return self.capacitor_charging()
def discharge_step(self, load_current: float):
"""One dt load step (imperative helper; optionally call without interrupts)."""
self.current = load_current
return self.load_discharging(load_current)
def leak_step(self):
"""One dt leakage-only step."""
return self.capacitor_selfdischarge()
|