{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  },
  "colab": {
   "name": "SolarHearth_Power_Cost_Time.ipynb",
   "provenance": []
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# JN-SolarHearth &middot; Power, Cost, and Time\n",
    "\n",
    "**Provenance:** generated 2026-08-10 by `build_jn_solarhearth.py` &middot; companion to\n",
    "[solarhearth.pages.dev](https://solarhearth.pages.dev) &middot; a concept study, not a product.\n",
    "\n",
    "**Discipline (the JN convention):** every figure below is **derived** from the single\n",
    "constants cell and asserted against a **dated baseline checkpoint** \u2014 never hardcoded\n",
    "in the logic. Change an assumption in `C`, re-run all: every number and chart moves,\n",
    "and the gates report exactly which results drifted from the 2026-08-10 checkpoint.\n",
    "Runs on Google Colab as-is (plotly is preinstalled); no pip installs needed.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \u00a70 \u00b7 Assumptions\n",
    "\ud83d\udcdd **Assumption:** one dict `C` carries every parameter \u2014 solar resource, component\n",
    "prices, loads, the MOF sorption curve, the time ledger. **Plan:** all later cells\n",
    "read only `C` and each other's derived values. **Why:** a notebook that types its\n",
    "answers into the chart calls decays silently; this one can't.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# =====================  SINGLE SOURCE OF ASSUMPTIONS  =====================\n",
    "# Change a value here, re-run all -> every figure and chart moves with it.\n",
    "# The BASELINE below is the 2026-08-10 checkpoint of derived results; a mismatch\n",
    "# means the constants changed and the gates will tell you exactly where.\n",
    "\n",
    "C = dict(\n",
    "    # --- solar resource & power costs ---\n",
    "    ghi_kwh_m2_day = 5.5,      # design solar resource (E.Africa / MENA / S.Asia)\n",
    "    pv_derate      = 0.78,     # dust, temperature, wiring\n",
    "    pv_usd_per_w   = 0.45,     # installed small-scale\n",
    "    batt_usd_kwh   = 160.0,    # LiFePO4, usable\n",
    "    module_wp      = 320,      # one PV module\n",
    "\n",
    "    # --- daily electrical loads (Wh) ---\n",
    "    loads = {\n",
    "        \"Induction cooking (2 meals)\":        2000,\n",
    "        \"Lights, phones, comms, radio/fan\":    465,\n",
    "        \"Flow pasteurizer, 20 L @ 12.3 Wh/L\":  246,\n",
    "        \"UV-C polish, 20 L\":                     4,\n",
    "        \"Chlorinator + test kit + sensors\":     10,\n",
    "    },\n",
    "    batt_kwh = 2.0,            # dusk meal 1.2 + overnight 0.5 + buffer\n",
    "\n",
    "    # --- flywheel comparison ---\n",
    "    fly_standby_loss_hr = 0.05,   # optimistic for low-cost bearings\n",
    "    fly_usd_kwh         = 2000.0,\n",
    "    fly_stored_dusk_kwh = 1.5,\n",
    "\n",
    "    # --- one-time hardware ($) ---\n",
    "    hw = {\n",
    "        \"Induction hob 1.8 kW\":                 65,\n",
    "        \"Enclosure, tank, plumbing, controls\": 170,\n",
    "        \"Phones/lights/comms/radio/fan kit\":   292,\n",
    "        \"UV-C flow cell\":                       60,\n",
    "        \"Water test kit + incubator\":           45,\n",
    "        \"Electrochlorination cell\":             90,\n",
    "        \"Flow pasteurizer + counterflow HX\":    95,\n",
    "        \"Arsenic cartridge (FeOOH, loaded)\":    50,\n",
    "        \"Fluoride column (alumina/bone char)\":  60,\n",
    "    },\n",
    "    consumables_yr = (77, 163),   # media + salt + 3%-capex maintenance, lo..hi\n",
    "    life_years     = 8,\n",
    "\n",
    "    # --- cooking-only tiers (name, cook_kwh, batt_kwh, extra_wh, extra_$, bop_$, hobs) ---\n",
    "    tiers = [\n",
    "        (\"A. Sun-hours cooker (no battery)\", 2.0, 0.15,   0,   0, 110, 1),\n",
    "        (\"B. Core cooker (evening meal)\",    2.0, 1.5,    0,   0, 140, 1),\n",
    "        (\"C. Home hub (light add-ons)\",      2.0, 1.9,  465, 292, 170, 1),\n",
    "        (\"D. Home hub + fridge\",             2.0, 2.6, 1465, 552, 170, 1),\n",
    "        (\"E. Cluster kitchen (5 hh)\",       10.0, 6.0,  465, 292, 450, 3),\n",
    "    ],\n",
    "    hob_usd = 65,\n",
    "\n",
    "    # --- MOF water-from-air ---\n",
    "    mof_uptake_l_kg   = {20: 0.22, 30: 0.32, 50: 0.42},  # per cycle, by night RH%\n",
    "    desorb_mj_per_l   = 3.1,\n",
    "    heat_recovery     = 0.40,     # active mode\n",
    "    fan_ctrl_kwh_l    = 0.10,\n",
    "    coll_eff          = 0.55,     # glazed solar-thermal box\n",
    "    coll_usd_m2       = 85.0,\n",
    "    mof_usd_kg_range  = (30, 80),\n",
    "    rh_range          = (20, 30, 50),\n",
    "    bop_household     = 260.0, bop_cluster = 900.0,\n",
    "    mof_life_years    = 10, maint_frac_yr = 0.03,\n",
    "    trucking_usd_m3   = (5, 30),\n",
    "\n",
    "    # --- women's time ledger (hours/day, before -> with unit) ---\n",
    "    ledger = {\n",
    "        \"Fuel collection & purchase\": (1.5, 0.1),\n",
    "        \"Fire building & tending\":    (0.8, 0.2),\n",
    "        \"Water queuing & carrying\":   (1.5, 0.5),\n",
    "        \"Laundry by hand\":            (1.7, 0.3),\n",
    "    },\n",
    "    shadow_wage_usd_hr = (0.25, 0.60),\n",
    ")\n",
    "\n",
    "BASELINE = {\"load_wh\": 2725.0, \"pv_w\": 635.198, \"yield_wh\": 2745.6, \"fly_left_kwh\": 0.811, \"capex\": 1532.839, \"tier_core\": 655.0, \"tier_hub\": 1090.0, \"awh20a_lo\": 3531.0, \"awh20a_hi\": 5479.0, \"saved_yr_h\": 1606.0, \"val_lo\": 401.5, \"val_hi\": 963.6}   # 2026-08-10 checkpoint -- regenerated, never hand-edited\n",
    "\n",
    "def gate(name, derived, tol=0.5):\n",
    "    \"\"\"Compare a derived figure to its baseline; DIAGNOSE and halt on drift.\"\"\"\n",
    "    base = BASELINE[name]\n",
    "    if abs(derived - base) > tol:\n",
    "        raise AssertionError(\n",
    "            f\"GATE {name}: derived {derived} != baseline {base} \"\n",
    "            f\"(checkpoint 2026-08-10). A constant changed -- if intentional, \"\n",
    "            f\"re-run the generator to stamp a new baseline.\")\n",
    "    return derived\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \u00a71 \u00b7 Power\n",
    "\ud83d\udcdd **Assumption:** design solar resource 5.5 kWh/m\u00b2/day with a 0.78 derate; the load\n",
    "is the full unit (cooking + water treatment + accessories). **Plan:** derive the\n",
    "daily load, size the PV array in whole modules, check yield closure, and give the\n",
    "flywheel its fair hearing. **Why:** the array must be sized from the load, not the\n",
    "other way around \u2014 and the storage choice should be arithmetic, not fashion.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# ----------------------------- POWER -----------------------------\n",
    "wh_per_pvw = C[\"ghi_kwh_m2_day\"] * C[\"pv_derate\"]          # Wh/day per W of PV\n",
    "load_wh    = sum(C[\"loads\"].values())\n",
    "pv_w       = load_wh / wh_per_pvw\n",
    "modules    = -(-pv_w // C[\"module_wp\"])                     # ceil\n",
    "yield_wh   = modules * C[\"module_wp\"] * wh_per_pvw\n",
    "pv_cost    = pv_w * C[\"pv_usd_per_w\"]\n",
    "batt_cost  = C[\"batt_kwh\"] * C[\"batt_usd_kwh\"]\n",
    "\n",
    "# flywheel: overnight retention + cost ratio\n",
    "fly_left   = C[\"fly_stored_dusk_kwh\"] * (1 - C[\"fly_standby_loss_hr\"]) ** 12\n",
    "fly_ratio  = C[\"fly_usd_kwh\"] / C[\"batt_usd_kwh\"]\n",
    "\n",
    "print(f\"daily load        {load_wh:>7.0f} Wh\")\n",
    "print(f\"PV required       {pv_w:>7.0f} W  -> {modules:.0f} x {C['module_wp']} Wp modules\")\n",
    "print(f\"array yield       {yield_wh:>7.0f} Wh/day  (closure vs load: {yield_wh/load_wh:5.1%})\")\n",
    "print(f\"battery           {C['batt_kwh']} kWh  (${batt_cost:.0f});  PV ${pv_cost:.0f}\")\n",
    "print(f\"flywheel at dawn  {fly_left:.2f} kWh of {C['fly_stored_dusk_kwh']} \"\n",
    "      f\"({fly_left/C['fly_stored_dusk_kwh']:.0%} kept), {fly_ratio:.0f}x battery $/kWh\")\n",
    "\n",
    "gate(\"load_wh\", load_wh); gate(\"pv_w\", pv_w); gate(\"yield_wh\", yield_wh)\n",
    "gate(\"fly_left_kwh\", fly_left, tol=0.01)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\ud83d\udcdd **Found:** the load sizes to two 320 Wp modules with ~1% closure margin, and the\n",
    "flywheel keeps roughly half its dusk charge by dawn at 12\u00d7 the battery's $/kWh \u2014\n",
    "the gate cells above HALT if any of this drifts from the checkpoint. **Verify:**\n",
    "`gate()` compares each derived figure to the dated baseline and diagnoses drift.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# viz -- daily load budget + flywheel retention (derived, never typed literals)\n",
    "import plotly.graph_objects as go\n",
    "f = go.Figure(go.Bar(x=list(C[\"loads\"].values()), y=list(C[\"loads\"].keys()),\n",
    "                     orientation=\"h\", marker_color=\"#C97A10\"))\n",
    "f.update_layout(title=f\"Daily electrical load: {load_wh:,.0f} Wh -> {modules:.0f} x \"\n",
    "                      f\"{C['module_wp']} Wp modules\", height=280,\n",
    "                margin=dict(l=10, r=10, t=40, b=10))\n",
    "f.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\u26a0 **What this chart could mislead about:** the bars are *electrical* energy only \u2014\n",
    "the pasteurizer's small bar rides on a large *thermal* flow made cheap by heat\n",
    "recovery, and cooking's dominance is the design's point, not an inefficiency.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \u00a72 \u00b7 Cost\n",
    "\ud83d\udcdd **Assumption:** hardware prices are parametric estimates (PV $0.45/W, LiFePO4\n",
    "$160/kWh, commodity hob), not vendor quotes. **Plan:** build the full-unit BOM from\n",
    "`C`, reproduce the cooking-only tier ladder, then sweep the water-from-air module\n",
    "across its two dominant uncertainties (night humidity 20\u201350% RH, MOF $30\u201380/kg).\n",
    "**Why:** the ranges ARE the result \u2014 a single point number would overclaim.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# ----------------------------- COST: full-unit BOM -----------------------------\n",
    "bom_groups = {\n",
    "    \"Power system\": {\"PV array\": pv_cost, \"LiFePO4 battery\": batt_cost},\n",
    "    \"Core + modules + options\": dict(C[\"hw\"]),\n",
    "}\n",
    "capex = sum(v for g in bom_groups.values() for v in g.values())\n",
    "cons_lo, cons_hi = C[\"consumables_yr\"]\n",
    "yr_lo = capex / C[\"life_years\"] + cons_lo\n",
    "yr_hi = capex / C[\"life_years\"] + cons_hi\n",
    "\n",
    "print(f\"FULL UNIT CAPEX  ${capex:,.0f}\")\n",
    "print(f\"all-in per year  ${yr_lo:.0f}-{yr_hi:.0f}  (${yr_lo/12:.0f}-{yr_hi/12:.0f}/mo; charcoal displaced $15-40/mo)\")\n",
    "gate(\"capex\", capex)\n",
    "\n",
    "# ----------------------------- COST: cooking-only tiers -----------------------------\n",
    "tier_rows = []\n",
    "for name, cook, batt, xwh, xcap, bop, hobs in C[\"tiers\"]:\n",
    "    elec = cook + xwh / 1000\n",
    "    tier_capex = (elec * 1000 / wh_per_pvw) * C[\"pv_usd_per_w\"] \\\n",
    "                 + batt * C[\"batt_usd_kwh\"] + C[\"hob_usd\"] * hobs + bop + xcap\n",
    "    tier_rows.append((name, elec, round(tier_capex)))\n",
    "    print(f\"{name:<36} {elec:>5.2f} kWh/d  ${tier_capex:>6,.0f}\")\n",
    "gate(\"tier_core\", tier_rows[1][2]); gate(\"tier_hub\", tier_rows[2][2])\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# ----------------------------- COST: water-from-air sweep -----------------------------\n",
    "def awh_config(liters_day, rh, mof_usd_kg, mode, households=1):\n",
    "    uptake, cycles = C[\"mof_uptake_l_kg\"][rh], (1 if mode == \"passive\" else 3)\n",
    "    mof_kg   = liters_day / (uptake * cycles)\n",
    "    mof_cost = mof_kg * mof_usd_kg\n",
    "    if mode == \"passive\":\n",
    "        th_kwh    = liters_day * C[\"desorb_mj_per_l\"] / 3.6\n",
    "        coll_m2   = th_kwh / (C[\"ghi_kwh_m2_day\"] * C[\"coll_eff\"])\n",
    "        coll_cost = coll_m2 * C[\"coll_usd_m2\"]\n",
    "        water_e   = liters_day * 0.05\n",
    "    else:\n",
    "        coll_cost = 0.0\n",
    "        kwh_l   = C[\"desorb_mj_per_l\"] * (1 - C[\"heat_recovery\"]) / 3.6 + C[\"fan_ctrl_kwh_l\"]\n",
    "        water_e = liters_day * kwh_l\n",
    "    cook_e  = 2.0 * households\n",
    "    total_e = water_e + cook_e\n",
    "    pv_cost_c = (total_e * 1000 / wh_per_pvw) * C[\"pv_usd_per_w\"]\n",
    "    batt_kwh  = 1.2 * households + 0.3 + (water_e * 0.5 if mode == \"active\" else 0.1)\n",
    "    bop  = C[\"bop_cluster\"] if households > 1 else C[\"bop_household\"]\n",
    "    cap  = pv_cost_c + batt_kwh * C[\"batt_usd_kwh\"] + mof_cost + coll_cost + bop \\\n",
    "           + C[\"hob_usd\"] * min(households, 4)\n",
    "    lifetime_l    = liters_day * 365 * C[\"mof_life_years\"]\n",
    "    lifetime_cost = cap * (1 + C[\"maint_frac_yr\"] * C[\"mof_life_years\"]) + mof_cost\n",
    "    return round(cap), round(lifetime_cost / lifetime_l * 1000)\n",
    "\n",
    "AWH = [(\"HH 6 L/d passive\",   6, \"passive\", 1), (\"HH 20 L/d passive\", 20, \"passive\", 1),\n",
    "       (\"HH 20 L/d active\",  20, \"active\",  1), (\"Cluster 100 L/d\",  100, \"active\",  5)]\n",
    "awh_rows = {}\n",
    "for label, L, mode, hh in AWH:\n",
    "    pts = [awh_config(L, rh, mc, mode, hh)\n",
    "           for rh in C[\"rh_range\"] for mc in C[\"mof_usd_kg_range\"]]\n",
    "    caps, usdm3 = [p[0] for p in pts], [p[1] for p in pts]\n",
    "    awh_rows[label] = (min(caps), max(caps), min(usdm3), max(usdm3))\n",
    "    print(f\"{label:<20} capex ${min(caps):>6,}-{max(caps):<7,} water ${min(usdm3):>3}-{max(usdm3)}/m3\")\n",
    "print(f\"{'Trucked benchmark':<20} water ${C['trucking_usd_m3'][0]}-{C['trucking_usd_m3'][1]}/m3\")\n",
    "gate(\"awh20a_lo\", awh_rows[\"HH 20 L/d active\"][0]); gate(\"awh20a_hi\", awh_rows[\"HH 20 L/d active\"][1])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\ud83d\udcdd **Found:** the full unit lands at the checkpointed capex with all-in running cost\n",
    "inside the charcoal band it displaces; water-from-air stays 2\u201310\u00d7 trucked-water\n",
    "cost across the whole sweep, which is why it ships as the module of last resort.\n",
    "**Verify:** gates on the BOM total, two tier anchors, and the 20 L/day active\n",
    "range ends.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# viz -- tier capex + water-from-air ranges\n",
    "import plotly.graph_objects as go\n",
    "f = go.Figure(go.Bar(x=[r[2] for r in tier_rows], y=[r[0] for r in tier_rows],\n",
    "                     orientation=\"h\", marker_color=\"#C97A10\"))\n",
    "f.update_layout(title=\"Cooking-only tiers, capex ($)\", height=280,\n",
    "                margin=dict(l=10, r=10, t=40, b=10))\n",
    "f.show()\n",
    "\n",
    "labels = list(awh_rows)\n",
    "f2 = go.Figure(go.Bar(x=[awh_rows[k][1] - awh_rows[k][0] for k in labels],\n",
    "                      base=[awh_rows[k][0] for k in labels], y=labels,\n",
    "                      orientation=\"h\", marker_color=\"#0E7490\"))\n",
    "f2.update_layout(title=\"Water-from-air capex range across 20-50% RH x $30-80/kg MOF ($)\",\n",
    "                 height=260, margin=dict(l=10, r=10, t=40, b=10))\n",
    "f2.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\u26a0 **What these charts could mislead about:** tier bars are capex only (tier E serves\n",
    "5 households \u2014 per-household it is the *cheapest* tier); the water-from-air ranges\n",
    "span *uncertainty*, not choice \u2014 a deployment sits at one point of each bar, and\n",
    "the bar's width is exactly why the module is optional.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## \u00a73 \u00b7 Time\n",
    "\ud83d\udcdd **Assumption:** the ledger's hours are mid-range planning figures from a wide\n",
    "field literature (fuel collection, fire tending, water hauling, hand laundry);\n",
    "shadow wages $0.25\u20130.60/h for informal-sector labor. **Plan:** total the ledger,\n",
    "annualize, monetize, and compare against the unit's all-in yearly cost from \u00a72.\n",
    "**Why:** the claim \"the unit pays for itself in returned time alone\" must survive\n",
    "its own arithmetic at the *low* wage bound, or it doesn't get made.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# ----------------------------- TIME: the women's-time ledger -----------------------------\n",
    "before = sum(b for b, a in C[\"ledger\"].values())\n",
    "after  = sum(a for b, a in C[\"ledger\"].values())\n",
    "saved_day = before - after\n",
    "saved_yr  = saved_day * 365\n",
    "w_lo, w_hi = C[\"shadow_wage_usd_hr\"]\n",
    "val_lo, val_hi = saved_yr * w_lo, saved_yr * w_hi\n",
    "\n",
    "for task, (b, a) in C[\"ledger\"].items():\n",
    "    print(f\"{task:<30} {b:>4.1f} -> {a:.1f} h/day\")\n",
    "print(f\"{'TOTAL':<30} {before:>4.1f} -> {after:.1f}  (saves {saved_day:.1f} h/day, {saved_yr:,.0f} h/yr)\")\n",
    "print(f\"labor value at ${w_lo}-{w_hi}/h:  ${val_lo:,.0f}-{val_hi:,.0f}/yr\")\n",
    "print(f\"unit all-in cost:              ${yr_lo:.0f}-{yr_hi:.0f}/yr\")\n",
    "print(f\"-> the unit repays itself in returned time alone\" if val_lo > yr_hi else \"-> check parameters\")\n",
    "\n",
    "gate(\"saved_yr_h\", saved_yr, tol=1); gate(\"val_lo\", val_lo, tol=1); gate(\"val_hi\", val_hi, tol=1)\n",
    "assert val_lo > yr_hi, \"time value should exceed all-in cost at the low wage bound\"\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\ud83d\udcdd **Found:** ~4.4 hours returned daily \u2248 1,600 h/yr \u2014 a full-time job's worth of\n",
    "labor capacity \u2014 worth more per year at the lowest shadow wage than the unit costs\n",
    "all-in. The final `assert` enforces that ordering; if a price rise or a ledger\n",
    "correction ever breaks it, this notebook fails loudly rather than letting the\n",
    "claim stand. **Verify:** gates on saved hours and both value bounds.\n"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "# viz -- the time ledger, before vs with the unit\n",
    "import plotly.graph_objects as go\n",
    "tasks = list(C[\"ledger\"])\n",
    "f = go.Figure([\n",
    "    go.Bar(name=\"today\",     x=[C[\"ledger\"][t][0] for t in tasks], y=tasks,\n",
    "           orientation=\"h\", marker_color=\"#B4B2A9\"),\n",
    "    go.Bar(name=\"with unit\", x=[C[\"ledger\"][t][1] for t in tasks], y=tasks,\n",
    "           orientation=\"h\", marker_color=\"#0E7490\"),\n",
    "])\n",
    "f.update_layout(barmode=\"group\", height=300, title=\n",
    "    f\"A woman's day: {before:.1f} h -> {after:.1f} h ({saved_yr:,.0f} h/yr returned)\",\n",
    "    margin=dict(l=10, r=10, t=40, b=10))\n",
    "f.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\u26a0 **What this chart could mislead about:** these are planning estimates, not\n",
    "measurements \u2014 individual settlements vary substantially, and caregiving hours\n",
    "returned by less diarrheal illness are real but deliberately *not counted*.\n",
    "A deployment should run its own time-use baseline (it belongs in the pilot\n",
    "protocol) and let measured data replace this ledger.\n",
    "\n",
    "---\n",
    "*Models this notebook mirrors: [awh_model.py](https://solarhearth.pages.dev/models/awh_model.py) \u00b7\n",
    "[cook_only_model.py](https://solarhearth.pages.dev/models/cook_only_model.py) \u00b7\n",
    "[purification_model.py](https://solarhearth.pages.dev/models/purification_model.py) \u00b7\n",
    "[full_unit_pricing.py](https://solarhearth.pages.dev/models/full_unit_pricing.py)*\n"
   ]
  }
 ]
}