Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified doc/assets/energy_average_vache.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/assets/energy_feeder_auto_q54.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified doc/assets/energy_q54.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 26 additions & 23 deletions doc/energy_analysis_writeup.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,17 @@ exist for those loads.

| Mechanism | Energy | Share |
|-----------|-------:|------:|
| Drive | 211.7 kJ | 61.7% |
| Lower Roller | 27.5 kJ | 8.0% |
| Flywheel | 26.1 kJ | 7.6% |
| Upper Roller | 25.6 kJ | 7.5% |
| Kicker | 18.8 kJ | 5.5% |
| Spindexer | 15.9 kJ | 4.6% |
| Compressor | 9.9 kJ | 2.9% |
| Turret | 6.9 kJ | 2.0% |
| Drive | 208.1 kJ | 63.1% |
| Lower Roller | 27.1 kJ | 8.2% |
| Flywheel | 26.0 kJ | 7.9% |
| Upper Roller | 24.2 kJ | 7.3% |
| Kicker | 12.7 kJ | 3.8% |
| Compressor | 9.9 kJ | 3.0% |
| Spindexer | 9.2 kJ | 2.8% |
| Turret | 6.9 kJ | 2.1% |
| Electronics | 5.3 kJ | 1.6% |
| Hood | 0.4 kJ | 0.1% |
| **Total** | **342.8 kJ** | |
| **Total** | **329.7 kJ** | |

Q54 was a high-energy match — drive consumption was notably higher than the
event average, consistent with aggressive teleop movement.
Expand All @@ -39,35 +40,37 @@ autonomous enable; curves averaged on a common 165 s grid.

| Mechanism | Avg Energy | Share |
|-----------|----------:|------:|
| Drive | 180.6 kJ | 55.4% |
| Flywheel | 30.4 kJ | 9.3% |
| Lower Roller | 26.9 kJ | 8.3% |
| Upper Roller | 24.3 kJ | 7.4% |
| Kicker | 22.4 kJ | 6.9% |
| Spindexer | 19.0 kJ | 5.8% |
| Compressor | 10.1 kJ | 3.1% |
| Turret | 6.5 kJ | 2.0% |
| Electronics | 5.2 kJ | 1.6% |
| Drive | 177.4 kJ | 57.6% |
| Flywheel | 30.2 kJ | 9.8% |
| Lower Roller | 26.2 kJ | 8.5% |
| Upper Roller | 23.6 kJ | 7.7% |
| Kicker | 16.7 kJ | 5.4% |
| Spindexer | 11.9 kJ | 3.9% |
| Compressor | 10.1 kJ | 3.3% |
| Turret | 6.4 kJ | 2.1% |
| Electronics | 5.2 kJ | 1.7% |
| Hood | 0.5 kJ | 0.2% |
| **Total** | **326.1 kJ** | |
| **Total** | **308.1 kJ** | |

---

## Key Observations

**Drive dominates.** At 55–62% of total energy, the drivetrain is by far the
**Drive dominates.** At 57–63% of total energy, the drivetrain is by far the
largest consumer. All growth is approximately linear, indicating sustained
driving throughout both auto and teleop with no extended idle periods.
driving throughout both auto and teleop with no extended idle periods. Kraken
regenerative braking recovers ~3.2–3.6 kJ per match (~1.8% of drive energy),
reducing net drive consumption slightly relative to gross current draw.

**Shooter system is the second-largest group.** Flywheel + rollers (Lower/Upper)
+ Kicker + Spindexer collectively consume ~113 kJ on average (35%), nearly all
+ Kicker + Spindexer collectively consume ~109 kJ on average (35%), nearly all
of which is teleop — the bands for these mechanisms steepen noticeably after the
auto/teleop boundary as the shooter spins up and cycles continuously.

**Compressor is non-trivial.** At ~10 kJ (3%), the compressor ranks above the
turret. The pneumatic system runs throughout the match to maintain pressure.

**Electronics overhead (5.2 kJ, 1.6%)** covers OrangePis, LEDs, RIO, radio,
**Electronics overhead (5.2 kJ, 1.7%)** covers OrangePis, LEDs, RIO, radio,
CANivore, and coprocessors. The ~1.5 A constant draw on PDH channels 14–15
(OrangePis/LEDs) accounts for roughly 60% of this group, with the remainder
split across channels 20–22 (RIO, radio, CANivore/coprocessors).
Expand Down
36 changes: 29 additions & 7 deletions scripts/energy_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@
"Electronics": [], # OrangePis/LEDs + RIO/radio/CANivore/coprocessors — sourced from PDH channels
}

# Mechanisms using Krakens (TalonFX/Phoenix 6) whose supply current can go
# negative during regenerative braking. Energy is integrated without clamping
# for these groups so regen recovery reduces the net energy total.
KRAKEN_GROUPS = {"Drive", "Flywheel", "Lower Roller", "Upper Roller"}

# ---------------------------------------------------------------------------
# Log reader

Expand Down Expand Up @@ -193,10 +198,15 @@ def interp_entry(


def cumulative_energy_J(
t_abs: np.ndarray, current: np.ndarray, voltage: np.ndarray
t_abs: np.ndarray, current: np.ndarray, voltage: np.ndarray,
clamp: bool = False,
) -> np.ndarray:
"""Trapezoid-integrate P=V*I; supply current clamped >=0. Returns J array."""
power = np.maximum(current, 0.0) * voltage
"""Trapezoid-integrate P=V*I. Returns J array.
clamp=True: floor supply current at 0 (use for non-regen sources like PDH channels).
clamp=False (default): allow negative current so regen braking reduces net energy.
"""
i = np.maximum(current, 0.0) if clamp else current
power = i * voltage
dE = 0.5 * (power[:-1] + power[1:]) * np.diff(t_abs)
return np.concatenate([[0.0], np.cumsum(dE)])

Expand All @@ -221,8 +231,8 @@ def process_match(

match_duration = match_end - auto_start # seconds

def to_common(current: np.ndarray) -> np.ndarray:
energy_kJ = cumulative_energy_J(t_abs, current, voltage) / 1000.0
def to_common(current: np.ndarray, clamp: bool = False) -> np.ndarray:
energy_kJ = cumulative_energy_J(t_abs, current, voltage, clamp=clamp) / 1000.0
t_elapsed = np.linspace(0.0, match_duration, len(t_abs))
return np.interp(COMMON_GRID, t_elapsed, energy_kJ,
left=0.0, right=energy_kJ[-1])
Expand All @@ -239,9 +249,21 @@ def to_common(current: np.ndarray) -> np.ndarray:
found = False
for entry in entries:
if entry in scalars:
total_current += interp_entry(scalars, entry, t_abs)
current = interp_entry(scalars, entry, t_abs)
# AKit delta-logs current only on value change. Motors idle at
# exactly 0 A produce no log entries, so the first sample in
# our window may be non-zero (e.g. mid-shoot for spindexer/
# kicker). interp_entry extrapolates that value back to t=0,
# falsely accumulating energy. Zero out everything before the
# first actual logged timestamp within this match window.
ts_all = scalars[entry][0]
in_window = ts_all[(ts_all >= auto_start - 1.0) & (ts_all <= match_end + 1.0)]
if len(in_window) > 0:
current[t_abs < in_window[0]] = 0.0
total_current += current
found = True
result[group] = to_common(total_current) if found else np.zeros_like(COMMON_GRID)
clamp = group not in KRAKEN_GROUPS
result[group] = to_common(total_current, clamp=clamp) if found else np.zeros_like(COMMON_GRID)

# PDH-sourced overhead group (climber + RIO/radio/coprocessors/LEDs)
electronics_i = extract_pdh_channel(arrays, PDH_ELECTRONICS_CHANNELS, t_abs)
Expand Down
113 changes: 113 additions & 0 deletions scripts/energy_feeder_auto_q54.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Spindexer + Kicker cumulative energy — VACHE Q54 autonomous only.
Reuses read_log / cumulative_energy_J from energy_analysis.py.
"""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
from energy_analysis import read_log, interp_entry, cumulative_energy_J

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

LOG_PATH = r"C:\Users\natel\wpilib\logs\VACHE\session_46\akit_26-03-22_14-57-55_vache_q54.wpilog"
AUTO_START = 150.011873
AUTO_END = 174.650002 # autonomous end == teleop start

GRID_DT = 0.02
VOLTAGE_ENTRY = "/RealOutputs/PDH/Voltage"
NOMINAL_V = 12.0

MECHANISMS = {
"Spindexer": "/Spindexer/CurrentAmps",
"Kicker": "/Kicker/CurrentAmps",
}

COLORS = {"Spindexer": "#4e9af1", "Kicker": "#f1884e"}


def main():
scalars, _ = read_log(LOG_PATH)

t_abs = np.arange(AUTO_START, AUTO_END + GRID_DT, GRID_DT)
t_elapsed = t_abs - AUTO_START # 0 → ~24.6 s

voltage = interp_entry(scalars, VOLTAGE_ENTRY, t_abs)
if voltage.max() < 1.0:
voltage = np.full_like(t_abs, NOMINAL_V)

energies = {}
for name, entry in MECHANISMS.items():
current = interp_entry(scalars, entry, t_abs)
# AKit delta-logs current only on value change. Motors idle at exactly 0 A
# produce no log entries, so the first sample in our window is non-zero
# (mid-shoot). interp_entry extrapolates that value back to t=0, falsely
# accumulating energy during the pre-shoot idle window.
# Fix: find the first timestamp *within this analysis window* and zero out
# everything before it.
if entry in scalars:
ts_all = scalars[entry][0]
in_window = ts_all[(ts_all >= AUTO_START - 1.0) & (ts_all <= AUTO_END + 1.0)]
if len(in_window) > 0:
current[t_abs < in_window[0]] = 0.0
energies[name] = cumulative_energy_J(t_abs, current, voltage, clamp=True) / 1000.0 # → kJ

# ── print summary ──────────────────────────────────────────────────────────
print("\nQ54 autonomous energy (spindexer + kicker):")
total = sum(e[-1] for e in energies.values())
for name, e in energies.items():
print(f" {name:12s} {e[-1]:.2f} kJ ({100*e[-1]/total:.1f}%)")
print(f" {'TOTAL':12s} {total:.2f} kJ")

# ── plot ───────────────────────────────────────────────────────────────────
fig, ax = plt.subplots(figsize=(10, 5))
fig.patch.set_facecolor("#1a1a2e")
ax.set_facecolor("#16213e")

stack = np.zeros_like(t_elapsed)
for name in ["Spindexer", "Kicker"]:
top = stack + energies[name]
lbl = f"{name} ({energies[name][-1]:.2f} kJ)"
ax.fill_between(t_elapsed, stack, top, color=COLORS[name], alpha=0.82, label=lbl)
ax.plot(t_elapsed, top, color=COLORS[name], linewidth=0.7)
stack = top

# annotate shoot window (feeder command starts at 160.321 s)
shoot_start = 160.321481 - AUTO_START # ≈ 10.3 s elapsed
shoot_end = 165.5 - AUTO_START # ≈ 15.5 s elapsed (current drops to idle)
ax.axvspan(shoot_start, shoot_end, color="white", alpha=0.07, label="Shoot window")
ax.axvline(shoot_start, color="white", linestyle="--", linewidth=0.9, alpha=0.6)
ax.axvline(shoot_end, color="white", linestyle="--", linewidth=0.9, alpha=0.6)
y_top = stack[-1]
ax.text(shoot_start + 0.2, y_top * 0.92, "shoot\nwindow",
color="white", fontsize=8, va="top", alpha=0.75)

ax.set_xlabel("Autonomous elapsed time (s)", color="white", fontsize=12)
ax.set_ylabel("Cumulative energy (kJ)", color="white", fontsize=12)
ax.set_title("VACHE Q54 — Spindexer + Kicker Energy (Autonomous only)",
color="white", fontsize=13)
ax.tick_params(colors="white")
ax.set_xlim(0, t_elapsed[-1])
ax.set_ylim(bottom=0)
for spine in ax.spines.values():
spine.set_edgecolor("#444466")
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x:.2f}"))

handles, labels = ax.get_legend_handles_labels()
ax.legend(handles[::-1], labels[::-1], loc="upper left", fontsize=10,
facecolor="#1a1a2e", edgecolor="#444466", labelcolor="white")

plt.tight_layout()
out = Path(__file__).parent.parent / "doc" / "assets" / "energy_feeder_auto_q54.png"
out.parent.mkdir(parents=True, exist_ok=True)
plt.savefig(out, dpi=150, facecolor=fig.get_facecolor())
print(f"\nSaved -> {out}")
plt.show()


if __name__ == "__main__":
main()
Loading