Resource_Analytics / generate_mock_data.py
pvanand's picture
Upload 18 files
cee8b9e verified
"""
EV Camper Mock Data Generator
==============================
Generates realistic power and water CSV exports following the discussed schema,
derived from data.json lookup tables.
Column naming convention:
*_kW instantaneous power (flow files: 1SEC, 1MIN, 15MIN)
*_kWh accumulated energy (energy files: 1H, 1DAY)
*_V voltage (instantaneous or averaged)
*_Ah amp-hours (battery state snapshot)
*_Pct percentage (battery / tank level)
*_Lpm litres per minute (water flow files: 1MIN, 15MIN)
*_L litres (water energy files: 1H, 1DAY, and tank level snapshots)
Output ZIP contains:
power/ -> 1SEC.csv, 1MIN.csv, 15MIN.csv, 1H.csv, 1DAY.csv
water/ -> 1MIN.csv, 15MIN.csv, 1H.csv, 1DAY.csv
Usage:
python generate_mock_data.py [--config data.json] [--out output] [--seed 42]
python generate_mock_data.py --user Glamper --people 2 --days 5 --temp Hot
"""
import json
import csv
import os
import math
import random
import zipfile
import argparse
from datetime import datetime, timedelta
from pathlib import Path
# ---------------------------------------------------------------------------
# CONSTANTS
# ---------------------------------------------------------------------------
GAL_TO_LITRES = 3.78541
MINS_PER_DAY = 1440
SOLAR_PANEL_AREA_M2 = 1.8 # ~330W panel footprint
BATTERY_NOMINAL_V = 48.0 # for kWh <-> Ah conversion
# ---------------------------------------------------------------------------
# DATA LOADER
# ---------------------------------------------------------------------------
def load_data(path: str) -> dict:
with open(path) as f:
return json.load(f)
# ---------------------------------------------------------------------------
# DAILY BUDGET CALCULATORS
# ---------------------------------------------------------------------------
def calc_power_budget(data: dict, user: str, people: int,
temp: str, hvac_hrs: float) -> dict:
"""
Returns expected kWh per day broken down by circuit + solar generation.
Derived from component voltage x amps tables and user profile runtimes.
"""
p = data["lookups"]["user_profiles"]["profiles"][user]
tb = p["time_based"]
cb = p["count_based"]
def watts(v, a): return v * a
# HVAC
hvac_kwh = data["lookups"]["hvac_energy_wh_day"][temp] / 1000.0
# Lighting (12V 12A)
lighting_kwh = watts(12, 12) * (tb["mins_per_day"].get("living_lighting", 300) / 60) / 1000
# Devices: always-on sensors/compute + active electronics
devices_kwh = (
5 * 12 * 24 # sensor idle
+ 1.25 * 12 * 24 # compute idle
+ 0.5 * 120 * tb["hrs_per_day"].get("living_electronics", 16)
) / 1000
# Fridge (24h)
fridge_kwh = watts(120, 0.5137) * 24 / 1000
# Water pump
meals = cb["meals_per_day"]["cooking"]
shower_cyc = cb["cycles_per_day_per_person"]["shower"] * people
toilet_cyc = cb["cycles_per_day_per_person"]["toilet"] * people
pump_mins = (tb["mins_per_meal"].get("cooking_pump", 3.625) * meals
+ tb["mins_per_cycle"].get("shower_water_pump", 6) * shower_cyc
+ 1.0 * toilet_cyc)
water_pump_kwh = watts(12, 8.5) * (pump_mins / 60) / 1000
# Cooking (stove + microwave + water heater)
cooking_kwh = (
watts(240, 12.5) * (tb["mins_per_meal"].get("cooking_stove", 15) * meals / 60)
+ watts(120, 8.333) * (tb["mins_per_meal"].get("cooking_microwave", 3.5) * meals / 60)
+ watts(240, 33.4) * ((tb["mins_per_meal"].get("cooking_water_heater", 3) * meals
+ tb["mins_per_cycle"].get("shower_duration", 6) * shower_cyc) / 60)
) / 1000
# Inverter load (TV as representative AC load)
inverter_kwh = watts(120, 0.7) * (tb["mins_per_day"].get("living_tv", 60) / 60) / 1000
# Solar generation
sol = data["lookups"]["solar"]
humidity = data["inputs"]["params"]["humidity"]["value"]
sunlight = data["inputs"]["params"]["sunlight"]["value"]
insolation = sol["insolation_wh_m2_day"][temp][humidity]
num_panels = (data["trailer_specs"]["specs"]["num_solar_panels"]["value"]
if "num_solar_panels" in data["trailer_specs"]["specs"] else 35)
solar_kwh = (insolation * num_panels * SOLAR_PANEL_AREA_M2
* sol["system_loss_factor"]
* sol["tilt_factor"][temp]
* sol["sunlight_factor"][sunlight]) / 1_000
solar_kwh = min(solar_kwh,
data["trailer_specs"]["specs"]["solar_capacity_kw"]["value"] * 6)
return {
"solar_kwh": round(solar_kwh, 3),
"hvac_kwh": round(hvac_kwh, 3),
"lighting_kwh": round(lighting_kwh, 3),
"devices_kwh": round(devices_kwh, 3),
"fridge_kwh": round(fridge_kwh, 3),
"water_pump_kwh": round(water_pump_kwh, 3),
"cooking_kwh": round(cooking_kwh, 3),
"inverter_kwh": round(inverter_kwh, 3),
}
def calc_water_budget(data: dict, user: str, people: int) -> dict:
"""Returns expected litres per day per circuit, converted from profile gallon tables."""
p = data["lookups"]["user_profiles"]["profiles"][user]
vb = p["volume_based"]
cb = p["count_based"]
meals = cb["meals_per_day"]["cooking"]
shower_cyc = cb["cycles_per_day_per_person"]["shower"] * people
toilet_cyc = cb["cycles_per_day_per_person"]["toilet"] * people
def g2l(g): return round(g * GAL_TO_LITRES, 3)
return {
"shower_L": g2l(vb["gal_per_cycle"]["shower_water"] * shower_cyc),
"toilet_L": g2l((vb["gal_per_cycle"]["toilet_sink_water"]
+ vb["gal_per_cycle"]["toilet_gravity_flush"]) * toilet_cyc),
"kitchen_L": g2l((vb["gal_per_meal"].get("cooking_kitchen_faucet", 1.5)
+ vb["gal_per_meal"].get("cooking_dishwasher_water", 1.25)) * meals
+ vb["gal_per_day"].get("living_cleaning", 0.625)
+ vb["gal_per_day"].get("living_drinking_water", 0.75) * people),
}
# ---------------------------------------------------------------------------
# TIME-OF-DAY SHAPE FUNCTIONS
# ---------------------------------------------------------------------------
def solar_curve(hour: float) -> float:
if hour < 6 or hour > 18:
return 0.0
return max(0.0, math.sin(math.pi * (hour - 6) / 12))
def activity_curve(hour: float) -> float:
return max(0.01,
math.exp(-0.5 * ((hour - 8.0) / 1.2) ** 2)
+ math.exp(-0.5 * ((hour - 19.5) / 1.5) ** 2) * 0.8)
def hvac_curve(hour: float, temp: str) -> float:
if temp == "Hot":
return (0.4 + 0.6 * math.sin(math.pi * max(0, hour - 9) / 12)
if 9 <= hour <= 21 else 0.2)
if temp == "Cold":
return 0.7 + 0.3 * (1 - solar_curve(hour))
return 0.4 + 0.1 * math.sin(math.pi * hour / 24)
def water_event_curve(hour: float) -> float:
return max(0.0,
math.exp(-0.5 * ((hour - 7.5) / 1.0) ** 2)
+ math.exp(-0.5 * ((hour - 19.0) / 1.0) ** 2) * 0.6)
def jitter(rng: random.Random, scale: float = 0.05) -> float:
return 1.0 + rng.gauss(0, scale)
# ---------------------------------------------------------------------------
# MINUTE-LEVEL SERIES BUILDERS
# ---------------------------------------------------------------------------
def build_power_minutes(budget: dict, temp: str, battery_cap_kwh: float,
start: datetime, num_days: int,
rng: random.Random) -> list[dict]:
"""
1-minute power rows.
Flow columns: *_kW | State columns: *_Ah, *_Pct, *_V
"""
solar_cap_kw = budget["solar_kwh"] / 6.0
hvac_mean = budget["hvac_kwh"] / 24
lighting_mean = budget["lighting_kwh"] / (300 / 60)
devices_mean = budget["devices_kwh"] / 24
fridge_mean = budget["fridge_kwh"] / 24
pump_mean = budget["water_pump_kwh"] / 2
cooking_mean = budget["cooking_kwh"] / 1.5
inverter_mean = budget["inverter_kwh"] / max(budget["inverter_kwh"] / (0.7 * 120 / 1000), 0.1)
battery_kwh = battery_cap_kwh * 0.80
rows = []
for m in range(num_days * MINS_PER_DAY):
ts = start + timedelta(minutes=m)
hour = ts.hour + ts.minute / 60.0
solar_kw = round(max(0, solar_cap_kw * solar_curve(hour) * rng.uniform(0.85, 1.05)), 4)
ac = activity_curve(hour)
hvac_kw = round(max(0, hvac_mean * hvac_curve(hour, temp) * jitter(rng, 0.08)), 4)
lighting_kw = round((max(0, lighting_mean * ac * jitter(rng, 0.05)) if 6 <= hour <= 23 else 0.002), 4)
devices_kw = round(max(0, devices_mean * jitter(rng, 0.04)), 4)
fridge_kw = round(max(0, fridge_mean * (0.7 + 0.6 * rng.random()) * jitter(rng, 0.03)), 4)
pump_kw = round(max(0, pump_mean * water_event_curve(hour) * jitter(rng, 0.15)), 4)
cooking_kw = round(max(0, cooking_mean * ac * jitter(rng, 0.20)) if ac > 0.3 else 0.0, 4)
inverter_kw = round(max(0, inverter_mean * ac * jitter(rng, 0.10)), 4)
total_load = hvac_kw + lighting_kw + devices_kw + fridge_kw + pump_kw + cooking_kw + inverter_kw
net = solar_kw - total_load
shore_kw = 0.0
if net < 0 and battery_kwh < abs(net) / 60 * 0.95:
shore_kw = round(abs(net) * 1.05, 4)
net = 0.0
battery_flow_kw = round(net, 4)
battery_kwh = max(0, min(battery_cap_kwh, battery_kwh + battery_flow_kw / 60))
unmetered_kw = round(max(0,
solar_kw + shore_kw
+ (abs(battery_flow_kw) if battery_flow_kw < 0 else 0)
- total_load
- (battery_flow_kw if battery_flow_kw > 0 else 0)
), 4)
rows.append({
"Time": ts.strftime("%Y-%m-%dT%H:%M:%SZ"),
"Solar_Flow_kW": solar_kw,
"Shore_Flow_kW": shore_kw,
"Battery_Flow_kW": battery_flow_kw,
"HVAC_Flow_kW": hvac_kw,
"Lighting_Flow_kW": lighting_kw,
"Devices_Flow_kW": devices_kw,
"Fridge_Flow_kW": fridge_kw,
"WaterPump_Flow_kW": pump_kw,
"Cooking_Flow_kW": cooking_kw,
"Inverter_Flow_kW": inverter_kw,
"Unmetered_Flow_kW": unmetered_kw,
"Battery_Level_Ah": round(battery_kwh * 1000 / BATTERY_NOMINAL_V, 1),
"Battery_Level_Pct": round(battery_kwh / battery_cap_kwh * 100, 2),
"Solar_Voltage_V": round(rng.uniform(36, 52) if solar_kw > 0 else 0.0, 1),
"Battery_Voltage_V": round(46 + (battery_kwh / battery_cap_kwh) * 6 + rng.gauss(0, 0.2), 2),
})
return rows
def build_water_minutes(budget: dict, fresh_cap_L: float, grey_cap_L: float,
black_cap_L: float,
start: datetime, num_days: int,
rng: random.Random) -> list[dict]:
"""
1-minute water rows.
Flow columns: *_Lpm | State columns: *_L
Black tank level is derived entirely from Toilet_Flow_Lpm:
100% of toilet flush volume enters the black tank.
Grey tank receives shower + kitchen waste only (90% of flow, 10% evaporation/splash).
"""
shower_rate = budget["shower_L"] / MINS_PER_DAY
toilet_rate = budget["toilet_L"] / MINS_PER_DAY
kitchen_rate = budget["kitchen_L"] / MINS_PER_DAY
fresh_L = fresh_cap_L * 0.95
grey_L = 0.0
black_L = 0.0 # starts empty; fills from toilet flow
rows = []
for m in range(num_days * MINS_PER_DAY):
ts = start + timedelta(minutes=m)
hour = ts.hour + ts.minute / 60.0
wc = water_event_curve(hour)
# Tank-fill event at 07:00 on day 1 only
inlet_Lpm = round(rng.uniform(8, 12), 3) if m == 420 else 0.0
shower_Lpm = round(max(0, shower_rate * wc * 2.5 * jitter(rng, 0.15)), 4)
kitchen_Lpm = round(max(0, kitchen_rate * wc * 2.0 * jitter(rng, 0.10)), 4)
toilet_Lpm = round(max(0, toilet_rate * wc * 2.0 * jitter(rng, 0.20)), 4)
pump_Lpm = shower_Lpm + kitchen_Lpm + toilet_Lpm
if fresh_L < pump_Lpm:
pump_Lpm = max(0, fresh_L)
shower_Lpm = round(pump_Lpm * 0.60, 4)
kitchen_Lpm = round(pump_Lpm * 0.25, 4)
toilet_Lpm = round(pump_Lpm * 0.15, 4)
unmetered_Lpm = round(max(0, pump_Lpm - shower_Lpm - kitchen_Lpm - toilet_Lpm), 4)
# Update tank levels
fresh_L = max(0, min(fresh_cap_L, fresh_L + inlet_Lpm - pump_Lpm))
grey_L = min(grey_cap_L, grey_L + (shower_Lpm + kitchen_Lpm) * 0.9)
black_L = min(black_cap_L, black_L + toilet_Lpm) # 100% of toilet → black tank
rows.append({
"Time": ts.strftime("%Y-%m-%dT%H:%M:%SZ"),
"Inlet_Flow_Lpm": inlet_Lpm,
"Pump_Flow_Lpm": round(pump_Lpm, 4),
"Shower_Flow_Lpm": shower_Lpm,
"Kitchen_Flow_Lpm": kitchen_Lpm,
"Toilet_Flow_Lpm": toilet_Lpm,
"Unmetered_Flow_Lpm": unmetered_Lpm,
"FreshTank_Level_L": round(fresh_L, 2),
"GreyTank_Level_L": round(grey_L, 2),
"BlackTank_Level_L": round(black_L, 2),
})
return rows
# ---------------------------------------------------------------------------
# RESAMPLING
# ---------------------------------------------------------------------------
def resample_power(rows: list[dict], interval_mins: int, mode: str = "mean") -> list[dict]:
"""
mode='mean' → kW (15MIN flow file)
mode='sum' → kWh (1H, 1DAY energy files) [kW × 1 min / 60 = kWh]
"""
CIRCUITS = ["HVAC", "Lighting", "Devices", "Fridge",
"WaterPump", "Cooking", "Inverter", "Unmetered"]
out = []
for i in range(0, len(rows), interval_mins):
bucket = rows[i: i + interval_mins]
if not bucket:
continue
first, last = bucket[0], bucket[-1]
def mean(col):
return round(sum(r[col] for r in bucket) / len(bucket), 4)
def to_kwh(col):
return round(sum(r[col] for r in bucket) / 60, 6)
def avg_v(col):
return round(sum(r[col] for r in bucket) / len(bucket), 2)
row = {"Time": first["Time"]}
if mode == "mean":
row["Solar_Flow_kW"] = mean("Solar_Flow_kW")
row["Shore_Flow_kW"] = mean("Shore_Flow_kW")
row["Battery_Flow_kW"] = mean("Battery_Flow_kW")
for c in CIRCUITS:
row[f"{c}_Flow_kW"] = mean(f"{c}_Flow_kW")
row["Battery_Level_Ah"] = last["Battery_Level_Ah"]
row["Battery_Level_Pct"] = last["Battery_Level_Pct"]
row["Solar_Voltage_V"] = avg_v("Solar_Voltage_V")
row["Battery_Voltage_V"] = avg_v("Battery_Voltage_V")
else:
row["Solar_Total_kWh"] = to_kwh("Solar_Flow_kW")
row["Shore_Total_kWh"] = to_kwh("Shore_Flow_kW")
# Battery split: charged (+) and discharged (-) as separate positive columns
row["Battery_Charged_kWh"] = round(
sum(r["Battery_Flow_kW"] for r in bucket if r["Battery_Flow_kW"] > 0) / 60, 6)
row["Battery_Discharged_kWh"] = round(
sum(abs(r["Battery_Flow_kW"]) for r in bucket if r["Battery_Flow_kW"] < 0) / 60, 6)
for c in CIRCUITS:
row[f"{c}_Total_kWh"] = to_kwh(f"{c}_Flow_kW")
row["Battery_Level_Ah"] = last["Battery_Level_Ah"]
row["Battery_Level_Pct"] = last["Battery_Level_Pct"]
row["Solar_Voltage_Avg_V"] = avg_v("Solar_Voltage_V")
row["Battery_Voltage_Avg_V"] = avg_v("Battery_Voltage_V")
out.append(row)
return out
def resample_water(rows: list[dict], interval_mins: int, mode: str = "mean",
fresh_cap_L: float = 378.5, grey_cap_L: float = 189.3,
black_cap_L: float = 170.3) -> list[dict]:
"""
mode='mean' → Lpm (15MIN flow file)
mode='sum' → L (1H, 1DAY energy files) [Lpm × 1 min = L]
Black tank level is a snapshot carried from 1MIN rows (derived from toilet flow).
"""
CIRCUITS = ["Inlet", "Pump", "Shower", "Kitchen", "Toilet", "Unmetered"]
out = []
for i in range(0, len(rows), interval_mins):
bucket = rows[i: i + interval_mins]
if not bucket:
continue
first, last = bucket[0], bucket[-1]
def mean_lpm(col):
return round(sum(r[col] for r in bucket) / len(bucket), 4)
def to_L(col):
return round(sum(r[col] for r in bucket), 4) # Lpm × 1 min = L
row = {"Time": first["Time"]}
if mode == "mean":
for c in CIRCUITS:
row[f"{c}_Flow_Lpm"] = mean_lpm(f"{c}_Flow_Lpm")
row["FreshTank_Level_L"] = last["FreshTank_Level_L"]
row["GreyTank_Level_L"] = last["GreyTank_Level_L"]
row["BlackTank_Level_L"] = last["BlackTank_Level_L"]
else:
for c in CIRCUITS:
row[f"{c}_Total_L"] = to_L(f"{c}_Flow_Lpm")
row["FreshTank_Level_L"] = last["FreshTank_Level_L"]
row["FreshTank_Level_Pct"] = round(last["FreshTank_Level_L"] / fresh_cap_L * 100, 2)
row["GreyTank_Level_L"] = last["GreyTank_Level_L"]
row["GreyTank_Level_Pct"] = round(last["GreyTank_Level_L"] / grey_cap_L * 100, 2)
row["BlackTank_Level_L"] = last["BlackTank_Level_L"]
row["BlackTank_Level_Pct"] = round(last["BlackTank_Level_L"] / black_cap_L * 100, 2)
out.append(row)
return out
# ---------------------------------------------------------------------------
# CSV WRITER
# ---------------------------------------------------------------------------
def write_csv(path: str, rows: list[dict]):
if not rows:
return
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
print(f" Wrote {len(rows):>6,} rows -> {path}")
# ---------------------------------------------------------------------------
# MAIN
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description="EV Camper Mock Data Generator")
parser.add_argument("--config", default="data.json", help="Path to data.json")
parser.add_argument("--out", default="output", help="Output directory")
parser.add_argument("--seed", type=int, default=42, help="Random seed")
parser.add_argument("--user", default=None, help="Glamper / Typical / Expert")
parser.add_argument("--people", type=int, default=None, help="Number of occupants")
parser.add_argument("--days", type=int, default=None, help="Trip duration in days")
parser.add_argument("--temp", default=None, help="Hot / Temperate / Cold")
parser.add_argument("--start", default="2026-02-18T00:00:00", help="Trip start (ISO datetime)")
args = parser.parse_args()
rng = random.Random(args.seed)
data = load_data(args.config)
params = data["inputs"]["params"]
specs = data["trailer_specs"]["specs"]
user = args.user or params["user_type"]["value"]
people = args.people or params["num_people"]["value"]
days = args.days or params["trip_duration_days"]["value"]
temp = args.temp or params["temperature"]["value"]
hvac_hrs = params["hvac_runtime_hrs"]["value"]
bat_cap_kwh = specs["battery_capacity_kwh"]["value"]
fresh_cap_gal = specs["freshwater_capacity_gal"]["value"]
grey_cap_gal = specs["greywater_capacity_gal"]["value"]
black_cap_gal = specs["blackwater_capacity_gal"]["value"]
fresh_cap_L = fresh_cap_gal * GAL_TO_LITRES
grey_cap_L = grey_cap_gal * GAL_TO_LITRES
black_cap_L = black_cap_gal * GAL_TO_LITRES
start = datetime.fromisoformat(args.start)
print(f"\n{'='*60}")
print(f" EV Camper Mock Data Generator")
print(f"{'='*60}")
print(f" Profile : {user} | People: {people} | Days: {days}")
print(f" Temp : {temp} | Start : {start.strftime('%Y-%m-%d')}")
print(f" Battery : {bat_cap_kwh} kWh | Fresh: {fresh_cap_gal} gal | Black: {black_cap_gal} gal")
print(f"{'='*60}\n")
pw = calc_power_budget(data, user, people, temp, hvac_hrs)
wat = calc_water_budget(data, user, people)
print(" Daily Power Budget:")
for k, v in pw.items(): print(f" {k:<22} {v:.3f} kWh")
print(f"\n Daily Water Budget:")
for k, v in wat.items(): print(f" {k:<22} {v:.1f} L")
print()
print(" Generating 1-minute base series...")
power_mins = build_power_minutes(pw, temp, bat_cap_kwh, start, days, rng)
water_mins = build_water_minutes(wat, fresh_cap_L, grey_cap_L, black_cap_L, start, days, rng)
out = Path(args.out)
# ------------------------------------------------------------------
# POWER FILES
# ------------------------------------------------------------------
pw_dir = out / "power"
print(" Resampling power files...")
# 1SEC: expand first 3h of 1-min rows to per-second with jitter
FLOW_COLS_KW = ["Solar_Flow_kW", "Shore_Flow_kW", "Battery_Flow_kW",
"HVAC_Flow_kW", "Lighting_Flow_kW", "Devices_Flow_kW",
"Fridge_Flow_kW", "WaterPump_Flow_kW", "Cooking_Flow_kW",
"Inverter_Flow_kW", "Unmetered_Flow_kW"]
sec_rows = []
for row in power_mins[:180]:
ts_base = datetime.strptime(row["Time"], "%Y-%m-%dT%H:%M:%SZ")
for s in range(60):
sr = dict(row)
sr["Time"] = (ts_base + timedelta(seconds=s)).strftime("%Y-%m-%dT%H:%M:%SZ")
for col in FLOW_COLS_KW:
sr[col] = round(max(0, row[col] * jitter(rng, 0.03)), 4)
sec_rows.append(sr)
write_csv(str(pw_dir / "1SEC.csv"), sec_rows)
write_csv(str(pw_dir / "1MIN.csv"), power_mins)
write_csv(str(pw_dir / "15MIN.csv"), resample_power(power_mins, 15, "mean"))
write_csv(str(pw_dir / "1H.csv"), resample_power(power_mins, 60, "sum"))
write_csv(str(pw_dir / "1DAY.csv"), resample_power(power_mins, MINS_PER_DAY,"sum"))
# ------------------------------------------------------------------
# WATER FILES
# ------------------------------------------------------------------
wt_dir = out / "water"
print(" Resampling water files...")
write_csv(str(wt_dir / "1MIN.csv"), water_mins)
write_csv(str(wt_dir / "15MIN.csv"), resample_water(water_mins, 15, "mean", fresh_cap_L, grey_cap_L, black_cap_L))
write_csv(str(wt_dir / "1H.csv"), resample_water(water_mins, 60, "sum", fresh_cap_L, grey_cap_L, black_cap_L))
write_csv(str(wt_dir / "1DAY.csv"), resample_water(water_mins, MINS_PER_DAY, "sum", fresh_cap_L, grey_cap_L, black_cap_L))
if __name__ == "__main__":
main()