SAFETY-90 review: count the start-up seconds like a gap
While the add-on is down the batteries are at 0 W (it sets 0 W when it stops; the ESP32 watchdog after 30 s), so the import before its first reading was the house, not the aim. Those seconds now count at gap_w(limit, the import seen first), and after a restart the restored reading's held 30 s is topped up the same way. Status row: "counted as high use". Tests take the pre-start import as the house (batteries at 0 W) and add restarts of 10/60/150 s with a restored averager. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ resort (SAFETY-91 test, O2).
|
|||||||
up to `gap_w` too. The guard then grades the quarter as usual, so it is not
|
up to `gap_w` too. The guard then grades the quarter as usual, so it is not
|
||||||
blind and gives what keeps the quarter at the aim.
|
blind and gives what keeps the quarter at the aim.
|
||||||
- **The bound.** The count is at or above the true import as long as, during
|
- **The bound.** The count is at or above the true import as long as, during
|
||||||
the gap, the house load stayed at or under the higher of the loads seen just
|
the gap, the house load (less solar) stayed at or under the higher of the loads seen just
|
||||||
before and just after it, or under the limit, and the batteries did not
|
before and just after it, or under the limit, and the batteries did not
|
||||||
start charging (during a gap they hold their last command, then go to 0 W).
|
start charging (during a gap they hold their last command, then go to 0 W).
|
||||||
It never counts the gap below the limit. A load that rises above all three
|
It never counts the gap below the limit. A load that rises above all three
|
||||||
@@ -33,14 +33,21 @@ resort (SAFETY-91 test, O2).
|
|||||||
- **While the gap lasts** nothing changes: the reading is too old to steer by,
|
- **While the gap lasts** nothing changes: the reading is too old to steer by,
|
||||||
the guard is blind (floor 0 W) and the "inputs missing" stop sets 0 W, as
|
the guard is blind (floor 0 W) and the "inputs missing" stop sets 0 W, as
|
||||||
before.
|
before.
|
||||||
- **Before the first reading** (start-up or restart mid-quarter) nothing is
|
- **Before the first reading** (start-up or restart mid-quarter) the
|
||||||
known, not even the house load, so a conservative count has no basis: the
|
add-on was down, so the batteries were at 0 W (it sets 0 W when it stops,
|
||||||
connection rating would make every restart drain at full power and raise
|
the ESP32 watchdog after 30 s). Those seconds count like a gap, at
|
||||||
"Load too high". Those seconds count **at the aim**, so the guard keeps the
|
`gap_w(the limit, the import seen first)`; after a restart the restored
|
||||||
rest of the quarter on the aim, and the page says so: the status row "Peak
|
reading's held `meter_max_age_s` is topped up the same way. The bound is
|
||||||
guard: part of this 15-minute period was not measured, counted as on
|
the gap's, with only the "after" side known. A load the batteries cannot
|
||||||
target" and the home line "Part of this 15-minute period was not measured." /
|
make up then raises "Load too high", which is true: that quarter is going
|
||||||
"Een deel van dit kwartier is niet gemeten." until the quarter ends.
|
over. The page says so: the status row "Peak guard: part of this
|
||||||
|
15-minute period was not measured, counted as high use" and the home line
|
||||||
|
"Part of this 15-minute period was not measured." / "Een deel van dit
|
||||||
|
kwartier is niet gemeten." until the quarter ends.
|
||||||
|
*Review:* the first version counted them at the aim, with "nothing is
|
||||||
|
known" as the reason. The first reading does show the load, and with the
|
||||||
|
batteries at 0 W the import was that load, not the aim: a 60 s restart
|
||||||
|
during a 6500 W HOLD landed 3653 W against 3450 W.
|
||||||
- The bill record (`quarter_blocks`) is unchanged: it still banks only what
|
- The bill record (`quarter_blocks`) is unchanged: it still banks only what
|
||||||
was measured. The estimate steers the guard and is never banked.
|
was measured. The estimate steers the guard and is never banked.
|
||||||
- The action log gets one `PEAK_GUARD_GAP` row per gap, with the seconds and
|
- The action log gets one `PEAK_GUARD_GAP` row per gap, with the seconds and
|
||||||
@@ -61,7 +68,8 @@ it, gap starting 300 s in; 10 s battery lag; aim 3450 W):
|
|||||||
| gap across the edge, next quarter, N=1 / N=2 | 4100.0 / 3621.2 | 3446.3 / 3382.2 |
|
| gap across the edge, next quarter, N=1 / N=2 | 4100.0 / 3621.2 | 3446.3 / 3382.2 |
|
||||||
| house 3000 -> 6500 W inside a 60 s gap, N=1 | 5255.6 | 3368.3 |
|
| house 3000 -> 6500 W inside a 60 s gap, N=1 | 5255.6 | 3368.3 |
|
||||||
| house 6500 -> 2000 W late in a 5 min gap, N=1 | 3629.2 | 3145.1 |
|
| house 6500 -> 2000 W late in a 5 min gap, N=1 | 3629.2 | 3145.1 |
|
||||||
| start-up 450 s into the quarter, N=1 (first half at the aim) | 4975.0 | 3450.0 |
|
| start-up 450 s in, N=1, house 5000 W (batteries at 0 W before it) | 5000.0 | 3450.0 |
|
||||||
|
| restart, down 60 s / 150 s, N=1, house 6500 W | 4039.1 / 5483.3 | 3450.0 / 3450.0 |
|
||||||
| meter never drops, N=1 and N=2 | | rows identical |
|
| meter never drops, N=1 and N=2 | | rows identical |
|
||||||
|
|
||||||
**Not changed.** The late-reading edge (SAFETY-91 test, O1): a reading late by
|
**Not changed.** The late-reading edge (SAFETY-91 test, O1): a reading late by
|
||||||
|
|||||||
@@ -1760,8 +1760,9 @@ a 900 s billing quarter and commands nothing, and this stops the import.
|
|||||||
**After a gap it goes on (SAFETY-90).** The seconds the meter missed count at
|
**After a gap it goes on (SAFETY-90).** The seconds the meter missed count at
|
||||||
the higher of the peak limit and the import seen just before and just after
|
the higher of the peak limit and the import seen just before and just after
|
||||||
the gap, with the batteries at 0 W. Seconds before the add-on's first reading
|
the gap, with the batteries at 0 W. Seconds before the add-on's first reading
|
||||||
count as on target, and the page says part of the period was not measured.
|
count the same way, at the import seen first, and the page says part of the
|
||||||
DECISIONS/SAFETY-90-peak-guard-after-a-gap.md has the bound.
|
period was not measured. DECISIONS/SAFETY-90-peak-guard-after-a-gap.md has the
|
||||||
|
bound.
|
||||||
|
|
||||||
**It trips, where the freeze detector only notifies,** and the captures are why.
|
**It trips, where the freeze detector only notifies,** and the captures are why.
|
||||||
On the healthy reference day (2026-08-23) it engages on **zero** of 95 quarters.
|
On the healthy reference day (2026-08-23) it engages on **zero** of 95 quarters.
|
||||||
|
|||||||
@@ -955,6 +955,12 @@ class Controller:
|
|||||||
self._gap_held_ws = 0.0
|
self._gap_held_ws = 0.0
|
||||||
self._gap_pre = None
|
self._gap_pre = None
|
||||||
self._unseen = None
|
self._unseen = None
|
||||||
|
# A held sample TARIFF-01 restored from before this restart: the
|
||||||
|
# averager credits it max_hold_s into the restart, at 0 W batteries.
|
||||||
|
_held = self.p1.averager.held_since
|
||||||
|
self._run_pre = (None if _held is None
|
||||||
|
else (_held, self.p1.averager.held_net_w))
|
||||||
|
self._unseen_held_ws = 0.0
|
||||||
self.peak_gap_s = 0.0
|
self.peak_gap_s = 0.0
|
||||||
self.peak_unmeasured_s = 0.0
|
self.peak_unmeasured_s = 0.0
|
||||||
self._peak_alarm_block = None
|
self._peak_alarm_block = None
|
||||||
@@ -2706,16 +2712,18 @@ class Controller:
|
|||||||
def _fill_peak_gaps(self, proj, now, reading):
|
def _fill_peak_gaps(self, proj, now, reading):
|
||||||
"""SAFETY-90: `proj` with a meter gap's uncounted seconds counted at
|
"""SAFETY-90: `proj` with a meter gap's uncounted seconds counted at
|
||||||
`peakguard.gap_w()` of the imports seen just before and just after it,
|
`peakguard.gap_w()` of the imports seen just before and just after it,
|
||||||
and those before this run's first reading at the aim (shown as not
|
and those before this run's first reading likewise at the import seen
|
||||||
measured). DECISIONS/SAFETY-90-peak-guard-after-a-gap.md."""
|
first (shown as not measured).
|
||||||
|
DECISIONS/SAFETY-90-peak-guard-after-a-gap.md."""
|
||||||
self.peak_gap_s = self.peak_unmeasured_s = 0.0
|
self.peak_gap_s = self.peak_unmeasured_s = 0.0
|
||||||
avg = self.p1.averager
|
avg = self.p1.averager
|
||||||
since = avg.held_since
|
since = avg.held_since
|
||||||
if proj is None or proj.block_start is None or since is None:
|
if proj is None or proj.block_start is None or since is None:
|
||||||
return proj
|
return proj
|
||||||
if self._unseen is None and self.p1.last is not None:
|
if self._unseen is None and self.p1.last is not None:
|
||||||
self._unseen = (proj.block_start, max(0.0, (
|
self._unseen = [proj.block_start, max(0.0, (
|
||||||
since - proj.block_start).total_seconds() - avg.elapsed_s))
|
since - proj.block_start).total_seconds() - avg.elapsed_s),
|
||||||
|
since, None]
|
||||||
seen = (None if self.grid is None or self.site_batt_w is None
|
seen = (None if self.grid is None or self.site_batt_w is None
|
||||||
else float(self.grid) + max(0.0, float(self.site_batt_w)))
|
else float(self.grid) + max(0.0, float(self.site_batt_w)))
|
||||||
if proj.block_start != self._gap_block:
|
if proj.block_start != self._gap_block:
|
||||||
@@ -2723,12 +2731,25 @@ class Controller:
|
|||||||
self._gap_filled_s = self._gap_held_ws = 0.0
|
self._gap_filled_s = self._gap_held_ws = 0.0
|
||||||
cap = float(self.o.get("peak_cap_w", 3500))
|
cap = float(self.o.get("peak_cap_w", 3500))
|
||||||
ceiling = peakguard.quarter_ceiling_w(cap, reading)[0]
|
ceiling = peakguard.quarter_ceiling_w(cap, reading)[0]
|
||||||
|
unseen_on = (self._unseen is not None
|
||||||
|
and self._unseen[0] == proj.block_start)
|
||||||
|
if unseen_on and self._unseen[3] is None and seen is not None:
|
||||||
|
# While the add-on was down the batteries were at 0 W, so the
|
||||||
|
# seconds before its first reading count like a gap, at the
|
||||||
|
# import seen first; a restored reading's held part is topped up.
|
||||||
|
self._unseen[3] = peakguard.gap_w(ceiling, seen)
|
||||||
|
if self._run_pre is not None:
|
||||||
|
s0, g0 = self._run_pre
|
||||||
|
h = (min(s0 + timedelta(seconds=avg.max_hold_s),
|
||||||
|
self._unseen[2])
|
||||||
|
- max(s0, proj.block_start)).total_seconds()
|
||||||
|
self._unseen_held_ws = max(0.0, h) * max(
|
||||||
|
0.0, self._unseen[3] - max(0.0, g0))
|
||||||
kw = dict(tail_s=(now - since).total_seconds(),
|
kw = dict(tail_s=(now - since).total_seconds(),
|
||||||
max_hold_s=avg.max_hold_s,
|
max_hold_s=avg.max_hold_s,
|
||||||
unseen_s=(self._unseen[1] if self._unseen is not None
|
unseen_s=self._unseen[1] if unseen_on else 0.0,
|
||||||
and self._unseen[0] == proj.block_start else 0.0),
|
unseen_w=(self._unseen[3] if unseen_on
|
||||||
unseen_w=peakguard.quarter_target_w(
|
and self._unseen[3] is not None else ceiling))
|
||||||
cap, self.peak_margin_w, reading))
|
|
||||||
_p, gap_s, _u = peakguard.fill_gaps(proj, gap_w=0.0, **kw)
|
_p, gap_s, _u = peakguard.fill_gaps(proj, gap_w=0.0, **kw)
|
||||||
if gap_s > self._gap_filled_s + peakguard.GAP_TOL_S:
|
if gap_s > self._gap_filled_s + peakguard.GAP_TOL_S:
|
||||||
self._gap_w = peakguard.gap_w(ceiling, self._gap_w,
|
self._gap_w = peakguard.gap_w(ceiling, self._gap_w,
|
||||||
@@ -2752,7 +2773,8 @@ class Controller:
|
|||||||
self._gap_pre = (since, float(self.grid))
|
self._gap_pre = (since, float(self.grid))
|
||||||
out, self.peak_gap_s, self.peak_unmeasured_s = peakguard.fill_gaps(
|
out, self.peak_gap_s, self.peak_unmeasured_s = peakguard.fill_gaps(
|
||||||
proj, gap_w=self._gap_w if self._gap_w is not None else ceiling,
|
proj, gap_w=self._gap_w if self._gap_w is not None else ceiling,
|
||||||
held_ws=self._gap_held_ws, **kw)
|
held_ws=self._gap_held_ws + (self._unseen_held_ws if unseen_on
|
||||||
|
else 0.0), **kw)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def _peak_over_drain(self, claims, maint_results) -> None:
|
def _peak_over_drain(self, claims, maint_results) -> None:
|
||||||
@@ -6458,7 +6480,7 @@ class Controller:
|
|||||||
peakguard.blind_kind(_pg.blind))})
|
peakguard.blind_kind(_pg.blind))})
|
||||||
elif self.peak_unmeasured_s > 0.0:
|
elif self.peak_unmeasured_s > 0.0:
|
||||||
# SAFETY-90: the start of this quarter came before the first
|
# SAFETY-90: the start of this quarter came before the first
|
||||||
# reading, and the guard counts it at the aim.
|
# reading, and the guard counts it as high use.
|
||||||
out.append({"ok": False, "warn": True, "id": "peak_guard",
|
out.append({"ok": False, "warn": True, "id": "peak_guard",
|
||||||
"text": messages.row_peak_guard_unmeasured()})
|
"text": messages.row_peak_guard_unmeasured()})
|
||||||
elif _pg.engaged:
|
elif _pg.engaged:
|
||||||
|
|||||||
@@ -794,9 +794,9 @@ def row_peak_guard_blind(why: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def row_peak_guard_unmeasured() -> str:
|
def row_peak_guard_unmeasured() -> str:
|
||||||
"""SAFETY-90. Before the first reading; that part counts as on target."""
|
"""SAFETY-90. Before the first reading; that part counts as high use."""
|
||||||
return ("Peak guard: part of this 15-minute period was not measured, "
|
return ("Peak guard: part of this 15-minute period was not measured, "
|
||||||
"counted as on target")
|
"counted as high use")
|
||||||
|
|
||||||
|
|
||||||
def row_peak_guard_on(floor_w, limit_w, *, shadow: bool = False) -> str:
|
def row_peak_guard_on(floor_w, limit_w, *, shadow: bool = False) -> str:
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
A gap past `meter_max_age_s` left seconds the quarter average never counted,
|
A gap past `meter_max_age_s` left seconds the quarter average never counted,
|
||||||
so the guard was blind for the rest of the quarter: a HOLD gave 0 W and the
|
so the guard was blind for the rest of the quarter: a HOLD gave 0 W and the
|
||||||
quarter raised the month peak (SAFETY-86 rig case 7b: 3964.8 W against a
|
quarter raised the month peak (SAFETY-86 rig case 7b: 3964.8 W against a
|
||||||
3450 W aim). Now the missed seconds count at `peakguard.gap_w()`, and seconds
|
3450 W aim). Now the missed seconds count at `peakguard.gap_w()`, and so do
|
||||||
before the add-on's first reading count at the aim, with a plain line on the
|
the seconds before the add-on's first reading, with a plain line on the
|
||||||
page. Drives the REAL Controller on a lagging battery (10 s), with the P1
|
page. Drives the REAL Controller on a lagging battery (10 s), with the P1
|
||||||
telegrams stopped for each gap. DECISIONS/SAFETY-90-peak-guard-after-a-gap.md.
|
telegrams stopped for each gap. DECISIONS/SAFETY-90-peak-guard-after-a-gap.md.
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ logging.disable(logging.WARNING)
|
|||||||
import opt11_identity as rig # noqa: E402
|
import opt11_identity as rig # noqa: E402
|
||||||
import safety84_identity as s84 # noqa: E402
|
import safety84_identity as s84 # noqa: E402
|
||||||
import app.p1 as p1mod # noqa: E402
|
import app.p1 as p1mod # noqa: E402
|
||||||
|
import app.tariff as tariff # noqa: E402
|
||||||
from app import homeview, messages, peakguard # noqa: E402
|
from app import homeview, messages, peakguard # noqa: E402
|
||||||
from app.maintenance import HOLD # noqa: E402
|
from app.maintenance import HOLD # noqa: E402
|
||||||
from app.p1 import make_sample, split_signed # noqa: E402
|
from app.p1 import make_sample, split_signed # noqa: E402
|
||||||
@@ -280,32 +281,77 @@ def grab(c, t):
|
|||||||
UI["nl"] = homeview.month_peak(c, c.cycle_at)["line"]
|
UI["nl"] = homeview.month_peak(c, c.cycle_at)["line"]
|
||||||
finally:
|
finally:
|
||||||
homeview._LANG[0] = "en"
|
homeview._LANG[0] = "en"
|
||||||
if t == 500.0:
|
if t == 453.0:
|
||||||
UI["later"] = (c.peak_unmeasured_s,
|
UI["later"] = (c.peak_unmeasured_s,
|
||||||
[r for r in c.checks() if r.get("id") == "peak_guard"])
|
[r for r in c.checks() if r.get("id") == "peak_guard"])
|
||||||
|
|
||||||
|
|
||||||
for n, socs, house in ((1, {"a": 99.0}, 6500.0),
|
def restored(t_dead, start=450.0):
|
||||||
(2, {"a": 99.0, "b": 15.0}, 5500.0)):
|
"""TARIFF-01 restores the averager at start-up: 0..t_dead measured at
|
||||||
rows, c = run(house, socs, 950.0, n=n, start=450.0,
|
the aim, the held sample at t_dead (the restart's first second)."""
|
||||||
on_cycle=grab if n == 1 else None)
|
def fake(self, averager, now=None):
|
||||||
orows, oc = without_fix(lambda: run(house, socs, 950.0, n=n, start=450.0))
|
bs = int(rig.BASE.timestamp() - start)
|
||||||
seen = sum(max(g, 0.0) for t, g in c.truth if t < 900) / QUARTER_S
|
averager.restore({"block": bs, "acc": AIM * t_dead,
|
||||||
old_seen = sum(max(g, 0.0) for t, g in oc.truth if t < 900) / QUARTER_S
|
"pp_acc": [AIM * t_dead], "elapsed": t_dead,
|
||||||
land = seen + 450.0 * AIM / QUARTER_S
|
"last_t": bs + t_dead, "last_net": AIM,
|
||||||
|
"last_pp": [AIM], "phases": 1})
|
||||||
|
self.restore_state = "resumed"
|
||||||
|
return self.restore_state
|
||||||
|
return fake
|
||||||
|
|
||||||
|
|
||||||
|
def startup(house, socs, n, *, t_dead=None, on_cycle=None):
|
||||||
|
"""The add-on starts 450 s in. While it was down the batteries were at
|
||||||
|
0 W (the ESP32 watchdog), so the meter saw the house: the quarter is
|
||||||
|
that, or the restored part at the aim, plus what this run measured."""
|
||||||
|
real = tariff.PeakTracker.restore
|
||||||
|
if t_dead is not None:
|
||||||
|
tariff.PeakTracker.restore = restored(t_dead)
|
||||||
|
try:
|
||||||
|
rows, c = run(house, socs, 455.0, n=n, start=450.0,
|
||||||
|
on_cycle=on_cycle)
|
||||||
|
finally:
|
||||||
|
tariff.PeakTracker.restore = real
|
||||||
|
pre = 450.0 * house if t_dead is None else (
|
||||||
|
t_dead * AIM + (450.0 - t_dead) * house)
|
||||||
|
return rows, c, (pre + sum(max(g, 0.0) for t, g in c.truth if t < 900)) \
|
||||||
|
/ QUARTER_S
|
||||||
|
|
||||||
|
|
||||||
|
for n, socs, house in ((1, {"a": 99.0}, 5000.0),
|
||||||
|
(2, {"a": 99.0, "b": 15.0}, 4500.0)):
|
||||||
|
rows, c, land = startup(house, socs, n, on_cycle=grab if n == 1 else None)
|
||||||
|
orows, _oc, old = without_fix(lambda: startup(house, socs, n))
|
||||||
TABLE.append((f"N={n} start-up mid-quarter", n, round(land, 1),
|
TABLE.append((f"N={n} start-up mid-quarter", n, round(land, 1),
|
||||||
round(old_seen + 450.0 * AIM / QUARTER_S, 1)))
|
round(old, 1)))
|
||||||
print(f" N={n}: measured part {seen:.1f} W + 450 s at the aim = "
|
check(f"⭐ N={n}: the first 450 s (house {house:.0f} W, batteries at 0 W) "
|
||||||
f"{land:.1f} W (before the fix {old_seen:.1f} W + the same)")
|
f"count at the import seen first; the quarter lands at or under "
|
||||||
check(f"⭐ N={n}: with the first 450 s at the aim the quarter lands at or "
|
f"the aim ({land:.1f} W, before the fix {old:.1f} W)",
|
||||||
f"under it; the holding A gives the need from the first minute",
|
land <= AIM + TOL and (n == 2 or old > AIM + 100))
|
||||||
land <= AIM + TOL and mean(rows, "a", 30, 450) >= house - AIM)
|
|
||||||
if n == 2:
|
if n == 2:
|
||||||
check("...and the held B gives nothing (O2 at start-up)",
|
check("...and the held B gives nothing (O2 at start-up); before the "
|
||||||
all(cmd(r)["b"] == 0.0 for r in rows if 1 <= r[0] < 450))
|
"fix it was drawn at 2500 W",
|
||||||
|
all(cmd(r)["b"] == 0.0 for r in rows if 1 <= r[0] < 450)
|
||||||
|
and mean(orows, "b", 30, 450) >= 2450)
|
||||||
check("...non-vacuous: before the fix A gave 0 W for the quarter",
|
check("...non-vacuous: before the fix A gave 0 W for the quarter",
|
||||||
mean(orows, "a", 30, 450) == 0)
|
mean(orows, "a", 30, 450) == 0)
|
||||||
|
|
||||||
|
rows, c, land = startup(6500.0, {"a": 99.0}, 1)
|
||||||
|
check(f"⭐ N=1, house 6500 W: more than the battery can make up; A gives its "
|
||||||
|
f"full 5000 W and the alarm says so ({land:.1f} W)",
|
||||||
|
mean(rows, "a", 30, 450) >= 4900
|
||||||
|
and any("PEAK_GUARD_SATURATED" in e for e in c.events))
|
||||||
|
|
||||||
|
for t_dead in (300.0, 390.0, 440.0):
|
||||||
|
rows, c, land = startup(6500.0, {"a": 99.0}, 1, t_dead=t_dead)
|
||||||
|
TABLE.append((f"N=1 restart, down {450 - t_dead:.0f} s", 1,
|
||||||
|
round(land, 1), round(without_fix(lambda: startup(
|
||||||
|
6500.0, {"a": 99.0}, 1, t_dead=t_dead))[2], 1)))
|
||||||
|
check(f"⭐ restart mid-quarter, down {450 - t_dead:.0f} s at house "
|
||||||
|
f"6500 W (the restored reading's held 30 s topped up too): the "
|
||||||
|
f"quarter lands at or under the aim ({land:.1f} W)",
|
||||||
|
land <= AIM + TOL)
|
||||||
|
|
||||||
rows, c = run(6500.0, {"a": 99.0}, 1350.0, n=1, start=450.0,
|
rows, c = run(6500.0, {"a": 99.0}, 1350.0, n=1, start=450.0,
|
||||||
gaps=[(1200, 1260)])
|
gaps=[(1200, 1260)])
|
||||||
a = quarter(c, 1)
|
a = quarter(c, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user