Skip to content

capacitor

amber.capacitor

CapacitorParams dataclass

Physical + timing constants.

Source code in amber/capacitor.py
 9
10
11
12
13
14
15
16
@dataclass(frozen=True)
class CapacitorParams:
    """Physical + timing constants."""

    dt: float = 1e-3  # s
    R_series: float = 5e3  # ohm
    R_leakage: float = 0.1e6  # ohm
    C: float = 300e-6  # F

Capacitor dataclass

SimPy-driven capacitor with: - charging (with leakage), - self-discharge, - load discharge with arbitrary current.

Source code in amber/capacitor.py
 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()

capacitor_charging

capacitor_charging() -> float

Charge toward voltage_source with leakage during one dt step.

Source code in amber/capacitor.py
63
64
65
66
67
68
69
70
71
72
73
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

capacitor_selfdischarge

capacitor_selfdischarge() -> float

Leakage-only decay during one dt step.

Source code in amber/capacitor.py
75
76
77
78
79
80
81
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

load_discharging

load_discharging(current: float) -> float

Discharge under an external load current for one dt step, with leakage accounted.

Source code in amber/capacitor.py
 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
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

charge_and_discharge

charge_and_discharge(load_current: float) -> float

Simultaneous charging from voltage_source and discharging to load. Net power model: voltage changes based on (harvesting - consumption).

Source code in amber/capacitor.py
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
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

charge_step

charge_step(source_voltage: float)

One dt charging step (imperative helper; optionally call without interrupts).

Source code in amber/capacitor.py
163
164
165
166
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()

discharge_step

discharge_step(load_current: float)

One dt load step (imperative helper; optionally call without interrupts).

Source code in amber/capacitor.py
168
169
170
171
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)

leak_step

leak_step()

One dt leakage-only step.

Source code in amber/capacitor.py
173
174
175
def leak_step(self):
    """One dt leakage-only step."""
    return self.capacitor_selfdischarge()