{
 "cells": [
  {
   "cell_type": "raw",
   "metadata": {
    "id": "aKILrYGWpRCg",
    "vscode": {
     "languageId": "raw"
    }
   },
   "source": [
    "---\n",
    "title: \"Data Collection 2 — Colab Notebook\"\n",
    "date: \"March 31, 2026\"\n",
    "visible-after: \"2026-09-25T20:00:00-06:00\"\n",
    "---\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- **Team Name:** `[Write in the name of your team.]`\n- **Authors:** `[Write in the name of the authors of this notebook.]`\n- **Cell Number:** `[Write in your cell number.]`\n- **J-V Apparatus Number:** `[Write in the number of your apparatus (JV1, JV2, or JV3) that you used for your measurements.]`\n- **EQE Apparatus Number:** `[Write in the number of the apparatus (EQE1, EQE2, or EQE3) that you used for your measurements.]`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "6INEocJnNpwa"
   },
   "source": [
    "## J-V Analysis\n",
    "\n",
    "1. Create a SINGLE plot of the J-V curves of the **forward scans** with data from all pixels.\n",
    "\n",
    "    a. Remember you can copy and paste code from previous notebooks and use AI tools.\n",
    "    b. Also remember that you measured I-V from the pixels, so you will need to determine the current density, J, first before plotting."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Your code here"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Question 1:** Discuss what you observe from looking at the comparison of your J-V curves. Do you have any pixels showing different behaviors?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "AzTlty7YOklE"
   },
   "source": [
    "2. Calculate the $J_{sc}$ and PCE values for each pixel and save them to a CSV file. The code below will also extract the standard error (SE) of the current at key measurement points, which we will use for uncertainty propagation later in the notebook. The code will loop through this week's JV measurements and output a CSV file with the following column headers:\n",
    "\n",
    "    `Pixel Number` | `Jsc (mA/cm^2)` | `SE_Jsc (mA/cm^2)` | `J_pmax (mA/cm^2)` | `V_pmax (V)` | `PCE` | `SE_Jpmax (mA/cm^2)` | `dV (V)` | `n_at_MPP`\n",
    "\n",
    "    **Note.** If you already have your own loop to do these calculations, feel free to use that. Just make sure your code also reads the `Forward_std (mA)` and `Forward_n` columns from the raw data so you can compute the standard error."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "zslxPh6ZOjis"
   },
   "outputs": [],
   "source": [
    "# Variables needed for J-V analysis\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import pandas as pd\n",
    "\n",
    "# Update variables as needed\n",
    "date_str = \"YYYY_MM_DD\"\n",
    "cell_id  = \"###\"\n",
    "area_cm2 = 0.14\n",
    "P_in = 99.8\n",
    "pixels = range(1, 9)\n",
    "\n",
    "# Column names matching the CSV output from measurement software\n",
    "# JV data columns: Voltage (V), Forward_mean (mA), Forward_std (mA), Forward_n,\n",
    "#                   Reverse_mean (mA), Reverse_std (mA), Reverse_n\n",
    "COL_V          = \"Voltage (V)\"\n",
    "COL_Forward_mA = \"Forward_mean (mA)\"\n",
    "COL_Fwd_std    = \"Forward_std (mA)\"\n",
    "COL_Fwd_n      = \"Forward_n\"\n",
    "\n",
    "# Store raw DataFrames and JV DataFrames for each pixel\n",
    "raw_data = {}\n",
    "jv_data  = {}\n",
    "\n",
    "# Load each pixel's data\n",
    "for n in pixels:\n",
    "    fname = f\"{date_str}_IV_cell{cell_id}_pixel{n}.csv\"\n",
    "    try:\n",
    "        df_raw = pd.read_csv(fname)\n",
    "        raw_data[n] = df_raw  # keep the full raw data for SE calculations\n",
    "\n",
    "        df_jv = pd.DataFrame({\n",
    "            \"Voltage (V)\": df_raw[COL_V].astype(float),\n",
    "            \"Current Density (mA/cm^2)\": df_raw[COL_Forward_mA].astype(float) / area_cm2\n",
    "        }).sort_values(\"Voltage (V)\").reset_index(drop=True)\n",
    "\n",
    "        # Add power density\n",
    "        df_jv[\"Power Density (mW/cm^2)\"] = (\n",
    "            df_jv[\"Voltage (V)\"] * df_jv[\"Current Density (mA/cm^2)\"]\n",
    "        )\n",
    "\n",
    "        jv_data[n] = df_jv\n",
    "        print(f\"[ok] Loaded pixel {n}\")\n",
    "\n",
    "    except FileNotFoundError:\n",
    "        print(f\"[skip] Pixel {n}: file not found ({fname})\")\n",
    "    except Exception as e:\n",
    "        print(f\"[skip] Pixel {n}: error reading {fname}: {e}\")\n",
    "\n",
    "# --- Calculate PCE parameters and SE-based uncertainties ---\n",
    "rows = []\n",
    "valid_pixels = sorted(jv_data.keys())\n",
    "\n",
    "for n in valid_pixels:\n",
    "    jv  = jv_data[n]\n",
    "    raw = raw_data[n].sort_values(COL_V).reset_index(drop=True)\n",
    "\n",
    "    V = jv[\"Voltage (V)\"].to_numpy()\n",
    "    J = jv[\"Current Density (mA/cm^2)\"].to_numpy()\n",
    "\n",
    "    # --- Standard PV parameters ---\n",
    "    Voc = float(np.interp(0.0, J, V))\n",
    "    Jsc = float(np.interp(0.0, V, J))\n",
    "    P   = V * J\n",
    "    idx_pmax = int(np.argmin(P))   # most negative power = MPP\n",
    "    Vmp = float(V[idx_pmax])\n",
    "    Jmp = float(J[idx_pmax])\n",
    "\n",
    "    denom = abs(Jsc) * Voc\n",
    "    FF  = (Vmp * abs(Jmp)) / denom if denom != 0 else np.nan\n",
    "    PCE = ((Voc * abs(Jsc) * FF) / P_in) * 100 if P_in != 0 else np.nan\n",
    "\n",
    "    # --- Standard Error at the MPP ---\n",
    "    std_mA_mpp = float(raw[COL_Fwd_std].iloc[idx_pmax])\n",
    "    n_mpp      = float(raw[COL_Fwd_n].iloc[idx_pmax])\n",
    "\n",
    "    if n_mpp > 1:\n",
    "        SE_I_mpp = std_mA_mpp / np.sqrt(n_mpp)    # SE of current in mA\n",
    "        SE_J_mpp = SE_I_mpp / area_cm2             # SE of current density in mA/cm^2\n",
    "    else:\n",
    "        SE_J_mpp = np.nan\n",
    "        print(f\"  [warn] Pixel {n}: n=1 at MPP, SE undefined\")\n",
    "\n",
    "    # --- Standard Error at V=0 (for Jsc uncertainty) ---\n",
    "    idx_v0 = int(np.argmin(np.abs(raw[COL_V].to_numpy())))\n",
    "    std_mA_v0 = float(raw[COL_Fwd_std].iloc[idx_v0])\n",
    "    n_v0      = float(raw[COL_Fwd_n].iloc[idx_v0])\n",
    "\n",
    "    if n_v0 > 1:\n",
    "        SE_Jsc = (std_mA_v0 / np.sqrt(n_v0)) / area_cm2\n",
    "    else:\n",
    "        SE_Jsc = np.nan\n",
    "\n",
    "    # --- Voltage uncertainty: half the voltage step size ---\n",
    "    if len(V) > 1:\n",
    "        voltage_step = float(np.median(np.abs(np.diff(V))))\n",
    "        dV = voltage_step / 2.0\n",
    "    else:\n",
    "        dV = np.nan\n",
    "\n",
    "    rows.append({\n",
    "        \"Pixel Number\":        n,\n",
    "        \"Jsc (mA/cm^2)\":      Jsc,\n",
    "        \"SE_Jsc (mA/cm^2)\":   SE_Jsc,\n",
    "        \"J_pmax (mA/cm^2)\":   Jmp,\n",
    "        \"V_pmax (V)\":         Vmp,\n",
    "        \"PCE\":                PCE,\n",
    "        \"SE_Jpmax (mA/cm^2)\": SE_J_mpp,\n",
    "        \"dV (V)\":             dV,\n",
    "        \"n_at_MPP\":           n_mpp,\n",
    "    })\n",
    "\n",
    "# Convert to DataFrame and write out as CSV\n",
    "out_name = f\"{date_str}_jsc_pce_cell{cell_id}.csv\"\n",
    "df_out = pd.DataFrame(rows)\n",
    "\n",
    "if df_out.empty:\n",
    "    print(\"[warn] No valid pixels found — did you run the loader above?\")\n",
    "else:\n",
    "    df_out.sort_values(\"Pixel Number\").to_csv(out_name, index=False)\n",
    "    print(f\"\\n[ok] Wrote {out_name}\")\n",
    "    display(df_out)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "c17bT0VbnWNv"
   },
   "source": [
    "**NOTE.** Even though your code gives you confirmation that it has successfully written your CSV, always open the file and check to make sure the data you meant to write is actually there."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "S6JZThq1W-0b"
   },
   "source": [
    "### Measurement Uncertainty and Error Bars\n",
    "\n",
    "Last week in lecture, we discussed error propagation and the importance of measurement uncertainty in the context of asking and answering scientific questions. To this end, we will calculate uncertainties in our JV measurements, propagate those uncertainties through our analysis, and then include error bars in our plots.\n",
    "\n",
    "#### Standard Error of the Mean\n",
    "\n",
    "When we take repeated measurements of the same quantity, we expect some variability from one measurement to the next. Two important statistical quantities help us characterize this variability:\n",
    "\n",
    "**Standard Deviation (std)** tells us how spread out individual measurements are. If you measured a current 10 times, the standard deviation describes the typical spread of those 10 values.\n",
    "\n",
    "**Standard Error of the Mean (SE)** tells us how precisely we know the *average* of those measurements:\n",
    "\n",
    "$$SE = \\frac{\\text{std}}{\\sqrt{n}}$$\n",
    "\n",
    "where $n$ is the number of repeated measurements. As we take more measurements, the standard error gets smaller — our estimate of the mean becomes more precise.\n",
    "\n",
    "**Why SE instead of std?** When we report $J_{pmax}$, $J_{sc}$, or any other parameter, we are using the *mean* value from our repeated measurements. The appropriate uncertainty for a mean value is the standard error, not the standard deviation.\n",
    "\n",
    "Our measurement software already computes the mean, standard deviation, and $n$ for each voltage point. The columns `Forward_std (mA)` and `Forward_n` in your J-V data files contain exactly what we need:\n",
    "\n",
    "$$SE_{J} = \\frac{\\texttt{Forward\\_std (mA)}}{\\sqrt{\\texttt{Forward\\_n}}} \\div \\texttt{pixel area}$$\n",
    "\n",
    "The code above already extracted these SE values at the maximum power point and at $V = 0$.\n",
    "\n",
    "#### Uncertainty Propagation for PCE\n",
    "\n",
    "We will propagate the uncertainty using the following formula:\n",
    "\n",
    "$$\\delta f = \\sqrt{\\left(\\frac{\\partial f}{\\partial x}\\right)^{2}(\\delta x)^{2} + \\left(\\frac{\\partial f}{\\partial y}\\right)^{2}(\\delta y)^{2} + \\left(\\frac{\\partial f}{\\partial z}\\right)^{2}(\\delta z)^{2}}$$\n",
    "\n",
    "Recall that:\n",
    "\n",
    "$$PCE = \\frac{|J_{pmax}| \\times V_{pmax}}{P_{in}} \\times 100\\%$$\n",
    "\n",
    "so the uncertainty in the PCE is:\n",
    "\n",
    "$$\\delta_{PCE} = PCE \\times \\sqrt{\\left(\\frac{\\delta J_{pmax}}{J_{pmax}}\\right)^2 + \\left(\\frac{\\delta V_{pmax}}{V_{pmax}}\\right)^2 + \\left(\\frac{\\delta P_{in}}{P_{in}}\\right)^2}$$\n",
    "\n",
    "where:\n",
    "- $\\delta J_{pmax}$ = standard error of the current density at the maximum power point (from our data)\n",
    "- $\\delta V_{pmax}$ = half the voltage step size (instrument resolution)\n",
    "- $\\delta P_{in}$ = $0.9 mW/cm^{2}$ (calibration uncertainty of the solar simulator)\n",
    "\n",
    "The code in the next cell computes this propagation automatically using the SE values extracted above.\n",
    "\n",
    "#### Which uncertainty matters most?\n",
    "\n",
    "Notice that we have three very different *types* of uncertainty entering the propagation:\n",
    "\n",
    "| Source | Type | What it represents |\n",
    "|--------|------|--------------------|\n",
    "| $\\delta J_{pmax}$ | **Statistical** (SE from repeated measurements) | How precisely we know the mean current at the MPP |\n",
    "| $\\delta V_{pmax}$ | **Instrumental** (voltage step resolution) | The finite spacing between voltage set-points |\n",
    "| $\\delta P_{in}$ | **Calibration** (solar simulator spec) | How well we know the incident light power |\n",
    "\n",
    "After running the code below, look at the relative uncertainty breakdown it prints. Which of these three terms dominates the total PCE uncertainty? What does that tell you about what limits the precision of your measurement — and what you would need to change to reduce it?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "BryjOyT71ba0"
   },
   "outputs": [],
   "source": [
    "# --- PCE Uncertainty Propagation using Standard Error ---\n",
    "df = pd.read_csv(f\"{date_str}_jsc_pce_cell{cell_id}.csv\")\n",
    "\n",
    "# Time variable — update depending on the week\n",
    "t = 168  # hours since initial measurement (update each week)\n",
    "df[\"Time (hr)\"] = t\n",
    "\n",
    "# Uncertainty in light power (from solar simulator calibration)\n",
    "dP_in = 0.9  # mW/cm^2\n",
    "\n",
    "# --- Propagate uncertainty for PCE ---\n",
    "J_abs   = np.abs(df[\"J_pmax (mA/cm^2)\"].to_numpy())\n",
    "V_vals  = np.abs(df[\"V_pmax (V)\"].to_numpy())\n",
    "PCE_vals = df[\"PCE\"].to_numpy()\n",
    "dJ      = df[\"SE_Jpmax (mA/cm^2)\"].to_numpy()\n",
    "dV      = df[\"dV (V)\"].to_numpy()\n",
    "\n",
    "# Relative uncertainties\n",
    "rel_J   = dJ / J_abs\n",
    "rel_V   = dV / V_vals\n",
    "rel_Pin = dP_in / P_in\n",
    "\n",
    "# PCE uncertainty (in percentage points, same units as PCE)\n",
    "df[\"PCE_uncertainty (%)\"] = PCE_vals * np.sqrt(rel_J**2 + rel_V**2 + rel_Pin**2)\n",
    "\n",
    "# Jsc uncertainty for reporting\n",
    "df[\"Jsc_uncertainty (mA/cm^2)\"] = df[\"SE_Jsc (mA/cm^2)\"]\n",
    "\n",
    "# Write final output\n",
    "out_name = f\"{date_str}_jsc_pce_uncertainties_cell{cell_id}.csv\"\n",
    "cols_to_write = [\n",
    "    \"Pixel Number\", \"Time (hr)\",\n",
    "    \"Jsc (mA/cm^2)\", \"Jsc_uncertainty (mA/cm^2)\",\n",
    "    \"PCE\", \"PCE_uncertainty (%)\"\n",
    "]\n",
    "df[cols_to_write].to_csv(out_name, index=False)\n",
    "print(f\"[ok] Wrote {out_name}\")\n",
    "display(df[cols_to_write])\n",
    "\n",
    "# Print a summary for quick review\n",
    "print(\"\\n--- Summary ---\")\n",
    "for _, row in df.iterrows():\n",
    "    pix = int(row[\"Pixel Number\"])\n",
    "    pce = row[\"PCE\"]\n",
    "    dpce = row[\"PCE_uncertainty (%)\"]\n",
    "    jsc = row[\"Jsc (mA/cm^2)\"]\n",
    "    djsc = row[\"Jsc_uncertainty (mA/cm^2)\"]\n",
    "    print(f\"  Pixel {pix}: PCE = {pce:.2f} +/- {dpce:.2f} %,  \"\n",
    "          f\"Jsc = {jsc:.3f} +/- {djsc:.3f} mA/cm^2\")\n",
    "\n",
    "# --- Relative uncertainty breakdown ---\n",
    "print(\"\\n--- Relative Uncertainty Breakdown (as %) ---\")\n",
    "print(f\"  {'Pixel':>5s}  {'δJ/J':>8s}  {'δV/V':>8s}  {'δP/P':>8s}  {'Total':>8s}\")\n",
    "for _, row in df.iterrows():\n",
    "    pix = int(row[\"Pixel Number\"])\n",
    "    rJ = row[\"SE_Jpmax (mA/cm^2)\"] / abs(row[\"J_pmax (mA/cm^2)\"]) * 100\n",
    "    rV = row[\"dV (V)\"] / abs(row[\"V_pmax (V)\"]) * 100\n",
    "    rP = dP_in / P_in * 100\n",
    "    total = np.sqrt((rJ/100)**2 + (rV/100)**2 + (rP/100)**2) * 100\n",
    "    print(f\"  {pix:5d}  {rJ:7.3f}%  {rV:7.3f}%  {rP:7.3f}%  {total:7.3f}%\")\n",
    "\n",
    "print(f\"\\n  Note: δJ/J is the SE-based measurement uncertainty (statistical).\")\n",
    "print(f\"        δV/V is the voltage step resolution (instrumental).\")\n",
    "print(f\"        δP/P is the solar simulator calibration (systematic).\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "NxdMm24dNLSH"
   },
   "source": [
    "#### Interpreting the Uncertainty Breakdown\n",
    "\n",
    "Look at the relative uncertainty table printed above. You should notice that:\n",
    "\n",
    "1. **$\\delta J / J$ is very small** (typically < 0.1%). With $n$ repeated measurements at each voltage, the standard error of the mean current is tiny — our instrument measures current very precisely and reproducibly.\n",
    "2. **$\\delta V / V$ and $\\delta P_{in} / P_{in}$ dominate** (each ~1%). These are *not* statistical uncertainties from repeated measurements — they are fixed properties of the instrument (the voltage step size chosen for the sweep) and the calibration of the solar simulator.\n",
    "\n",
    "This reveals something important about this experiment: **the precision of the PCE measurement is limited by instrument resolution and calibration, not by measurement repeatability.** Taking more current measurements (increasing $n$) would make $\\delta J / J$ even smaller, but would barely change the total uncertainty because the other two terms dominate."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Question 2:** Looking at the breakdown, which term contributes the most to the total PCE uncertainty? If you wanted to cut the PCE uncertainty in half, what would you need to change about the measurement — and what would *not* help?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "zvbkD3AGMg-4"
   },
   "source": [
    "3. **Repeat the J-V analysis above for your other weeks of data.** For each week, upload the raw JV CSV files from your team's shared Google Drive folder, update `date_str` and `t`, and re-run the code to generate the uncertainty CSV files. (The originals will remain in Google Drive — uploading just creates a working copy in Colab.)\n",
    "\n",
    "    Once you have uncertainty CSV files for all of your weeks, you are ready to plot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "CmQ8ZVCfMgC6"
   },
   "outputs": [],
   "source": [
    "# Add CSV files you want to have on the plot\n",
    "files = [\n",
    "    \"YYYY_MM_DD_jsc_pce_uncertainties_cell###.csv\",\n",
    "    \"YYYY_MM_DD_jsc_pce_uncertainties_cell###.csv\",\n",
    "    # etc,\n",
    "]\n",
    "\n",
    "# Load and combine data, skipping any missing files\n",
    "dfs = []\n",
    "for f in files:\n",
    "    try:\n",
    "        d = pd.read_csv(f)\n",
    "        d[\"source_file\"] = f  # optional: helps debug\n",
    "        dfs.append(d)\n",
    "    except FileNotFoundError:\n",
    "        print(f\"[skip] missing file: {f}\")\n",
    "\n",
    "if not dfs:\n",
    "    raise SystemExit(\"No data files loaded. Check file names above.\")\n",
    "\n",
    "all_df = pd.concat(dfs, ignore_index=True)\n",
    "\n",
    "# Keep only the columns we need and make them be numbers. You can add additional details from the input files if you want.\n",
    "need = [\"Pixel Number\", \"Time (hr)\", \"PCE\", \"PCE_uncertainty (%)\"]\n",
    "missing = [c for c in need if c not in all_df.columns]\n",
    "if missing:\n",
    "    raise ValueError(f\"Missing required columns in data: {missing}\")\n",
    "\n",
    "all_df[\"Pixel Number\"] = pd.to_numeric(all_df[\"Pixel Number\"], errors=\"coerce\").astype(\"Int64\")\n",
    "all_df[\"Time (hr)\"]    = pd.to_numeric(all_df[\"Time (hr)\"], errors=\"coerce\")\n",
    "all_df[\"PCE\"]          = pd.to_numeric(all_df[\"PCE\"], errors=\"coerce\")\n",
    "all_df[\"PCE_uncertainty (%)\"] = pd.to_numeric(all_df[\"PCE_uncertainty (%)\"], errors=\"coerce\")\n",
    "\n",
    "# Drop rows missing basics and dedupe (keep last if duplicates exist)\n",
    "all_df = (\n",
    "    all_df.dropna(subset=[\"Pixel Number\", \"Time (hr)\", \"PCE\"])\n",
    "          .drop_duplicates(subset=[\"Pixel Number\", \"Time (hr)\"], keep=\"last\")\n",
    ")\n",
    "\n",
    "# Determine which pixels actually appear\n",
    "pixels_present = sorted(all_df[\"Pixel Number\"].dropna().unique().astype(int))\n",
    "\n",
    "# Make a plot\n",
    "fig, ax = plt.subplots(figsize=(9, 6))\n",
    "\n",
    "markers = [\"o\", \"s\", \"^\", \"D\", \"v\", \">\", \"x\", \"*\"]\n",
    "\n",
    "for i, pix in enumerate(pixels_present):\n",
    "    d = all_df[all_df[\"Pixel Number\"] == pix].sort_values(\"Time (hr)\")\n",
    "    if d.empty:\n",
    "        continue\n",
    "\n",
    "    marker = markers[i % len(markers)]  # cycle markers if many pixels\n",
    "\n",
    "    ax.errorbar(\n",
    "        d[\"Time (hr)\"].to_numpy(),\n",
    "        d[\"PCE\"].to_numpy(),\n",
    "        yerr=d[\"PCE_uncertainty (%)\"].to_numpy(),\n",
    "        fmt=marker,\n",
    "        capsize=4,\n",
    "        label=f\"Pixel {pix}\"\n",
    "    )\n",
    "\n",
    "# Update legend/labels as desired\n",
    "ax.set_xlabel(\"Time (hr)\")\n",
    "ax.set_ylabel(\"PCE (%)\")\n",
    "ax.set_title(f\"PCE vs Time - Cell {cell_id}\")\n",
    "ax.grid(True, alpha=0.4)\n",
    "ax.legend(fontsize=8, title=\"Pixels\", ncols=2)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "6w_eKDzvcafc"
   },
   "source": [
    "Congratulations! That was a bunch of new code, but you will have to make minimal edits to use it in upcoming weeks."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "y1M_zkjD1ba0"
   },
   "source": [
    "**Question 3:** Discuss what you observe from looking at the PCE over time. Has anything changed over the past few weeks?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "_sCRfnTYdnfP"
   },
   "source": [
    "### EQE Analysis with Uncertainty\n",
    "\n",
    "The External Quantum Efficiency at each wavelength is:\n",
    "\n",
    "$$EQE(\\lambda) = \\frac{I(\\lambda)}{P(\\lambda)} \\times \\frac{hc}{e\\lambda}$$\n",
    "\n",
    "where $I(\\lambda)$ is the measured photocurrent and $P(\\lambda)$ is the incident optical power at wavelength $\\lambda$.\n",
    "\n",
    "Since EQE is a ratio of two measured quantities (each with their own uncertainty), we can propagate the uncertainties using the same approach as for PCE:\n",
    "\n",
    "$$\\frac{\\delta EQE}{EQE} = \\sqrt{\\left(\\frac{\\delta I}{I}\\right)^2 + \\left(\\frac{\\delta P}{P}\\right)^2}$$\n",
    "\n",
    "where:\n",
    "- $\\delta I = SE_I = \\frac{\\texttt{Current\\_std (nA)}}{\\sqrt{n_I}}$\n",
    "- $\\delta P = SE_P = \\frac{\\texttt{Power\\_std (uW)}}{\\sqrt{n_P}}$\n",
    "\n",
    "This gives us an uncertainty band around each EQE curve, allowing us to assess whether changes in EQE between weeks are statistically significant.\n",
    "\n",
    "As a reminder, the EQE data files have the following columns:\n",
    "- Power data: `Wavelength (nm)`, `Power_mean (uW)`, `Power_std (uW)`, `n`\n",
    "- Current data: `Wavelength (nm)`, `Current_mean (nA)`, `Current_std (nA)`, `n`\n",
    "1. The code below calculates EQE and its uncertainty for each pixel and saves the results to a CSV file.\n",
    "2. The plotting cells that follow create EQE curves with shaded uncertainty bands."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "lsXvTo42NLSI"
   },
   "outputs": [],
   "source": [
    "# --- EQE Calculation with Standard Error Uncertainty ---\n",
    "\n",
    "# Physical constants\n",
    "h = 6.62607015e-34       # Planck's constant (J*s)\n",
    "c = 3.0e8                # Speed of light (m/s)\n",
    "e_charge = 1.602176634e-19  # Elementary charge (C)\n",
    "\n",
    "# Load power data (shared across all pixels for one apparatus)\n",
    "power_fname = f\"{date_str}_power_cell{cell_id}.csv\"\n",
    "try:\n",
    "    power_data = pd.read_csv(power_fname)\n",
    "    print(f\"[ok] Loaded power data: {power_fname}\")\n",
    "except FileNotFoundError:\n",
    "    print(f\"[error] Power file not found: {power_fname}\")\n",
    "    print(\"  Update the filename above to match your power data file.\")\n",
    "\n",
    "wavelength_nm = power_data[\"Wavelength (nm)\"].to_numpy()\n",
    "wavelength_m  = wavelength_nm * 1e-9\n",
    "\n",
    "P_mean_uW = power_data[\"Power_mean (uW)\"].to_numpy()\n",
    "P_std_uW  = power_data[\"Power_std (uW)\"].to_numpy()\n",
    "n_P       = power_data[\"n\"].to_numpy().astype(float)\n",
    "\n",
    "# SE of power\n",
    "SE_P_uW = np.where(n_P > 1, P_std_uW / np.sqrt(n_P), np.nan)\n",
    "\n",
    "# Build EQE results dataframe\n",
    "eqe_df = pd.DataFrame({\"Wavelength (nm)\": wavelength_nm})\n",
    "\n",
    "for pix in pixels:\n",
    "    curr_fname = f\"{date_str}_current_cell{cell_id}_pixel{pix}.csv\"\n",
    "    try:\n",
    "        curr_data = pd.read_csv(curr_fname)\n",
    "\n",
    "        I_mean_nA = curr_data[\"Current_mean (nA)\"].to_numpy()\n",
    "        I_std_nA  = curr_data[\"Current_std (nA)\"].to_numpy()\n",
    "        n_I       = curr_data[\"n\"].to_numpy().astype(float)\n",
    "\n",
    "        # SE of current\n",
    "        SE_I_nA = np.where(n_I > 1, I_std_nA / np.sqrt(n_I), np.nan)\n",
    "\n",
    "        # Convert to SI units\n",
    "        I_mean_A = I_mean_nA * 1e-9\n",
    "        SE_I_A   = SE_I_nA * 1e-9\n",
    "        P_mean_W = P_mean_uW * 1e-6\n",
    "        SE_P_W   = SE_P_uW * 1e-6\n",
    "\n",
    "        # EQE calculation\n",
    "        eqe_vals = (I_mean_A / P_mean_W) * (h * c) / (e_charge * wavelength_m)\n",
    "\n",
    "        # EQE uncertainty propagation: dEQE/EQE = sqrt((dI/I)^2 + (dP/P)^2)\n",
    "        rel_I = SE_I_A / np.abs(I_mean_A)\n",
    "        rel_P = SE_P_W / np.abs(P_mean_W)\n",
    "        eqe_unc = np.abs(eqe_vals) * np.sqrt(rel_I**2 + rel_P**2)\n",
    "\n",
    "        eqe_df[f\"eqe_pix{pix}\"]     = eqe_vals\n",
    "        eqe_df[f\"eqe_unc_pix{pix}\"] = eqe_unc\n",
    "\n",
    "        print(f\"[ok] Pixel {pix}: peak EQE = {np.nanmax(eqe_vals):.4f} \"\n",
    "              f\"({np.nanmax(eqe_vals)*100:.1f}%)\")\n",
    "    except FileNotFoundError:\n",
    "        print(f\"[skip] Pixel {pix}: current file not found ({curr_fname})\")\n",
    "    except Exception as e:\n",
    "        print(f\"[skip] Pixel {pix}: {e}\")\n",
    "\n",
    "# Write to CSV\n",
    "eqe_out = f\"{date_str}_eqe_cell{cell_id}.csv\"\n",
    "eqe_df.to_csv(eqe_out, index=False)\n",
    "print(f\"\\n[ok] Wrote {eqe_out}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "5ULIIk0pNLSI"
   },
   "outputs": [],
   "source": [
    "# --- EQE Plots with Shaded Uncertainty Bands (This Week) ---\n",
    "eqe_df_plot = pd.read_csv(f\"{date_str}_eqe_cell{cell_id}.csv\")\n",
    "wl = eqe_df_plot[\"Wavelength (nm)\"].to_numpy()\n",
    "\n",
    "fig, axes = plt.subplots(4, 2, figsize=(12, 16))\n",
    "axes_flat = axes.flatten()\n",
    "\n",
    "for i in range(8):\n",
    "    ax = axes_flat[i]\n",
    "    pix = i + 1\n",
    "    col_eqe = f\"eqe_pix{pix}\"\n",
    "    col_unc = f\"eqe_unc_pix{pix}\"\n",
    "\n",
    "    if col_eqe not in eqe_df_plot.columns:\n",
    "        ax.set_visible(False)\n",
    "        continue\n",
    "\n",
    "    eqe_vals = eqe_df_plot[col_eqe].to_numpy()\n",
    "    ax.plot(wl, eqe_vals, 'b-', label=\"EQE\")\n",
    "\n",
    "    if col_unc in eqe_df_plot.columns:\n",
    "        eqe_unc = eqe_df_plot[col_unc].to_numpy()\n",
    "        ax.fill_between(wl,\n",
    "                        eqe_vals - eqe_unc,\n",
    "                        eqe_vals + eqe_unc,\n",
    "                        alpha=0.3, color='blue',\n",
    "                        label=\"SE uncertainty\")\n",
    "\n",
    "    ax.set_title(f\"Pixel {pix}\")\n",
    "    ax.set_xlabel(\"Wavelength (nm)\")\n",
    "    ax.set_ylabel(\"EQE\")\n",
    "    ax.legend(fontsize=8)\n",
    "    ax.set_ylim(bottom=0)\n",
    "\n",
    "plt.suptitle(f\"EQE with Uncertainty — {date_str} Cell {cell_id}\", fontsize=14)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "id": "IZ0gXPyKNLSI"
   },
   "outputs": [],
   "source": [
    "# --- Multi-Week EQE Comparison with Uncertainty Bands ---\n",
    "\n",
    "# Add your EQE CSV files here (update filenames each week)\n",
    "eqe_files = {\n",
    "    \"Week 5\": \"YYYY_MM_DD_eqe_cell###.csv\",\n",
    "    \"Week 6\": \"YYYY_MM_DD_eqe_cell###.csv\",\n",
    "    \"Week 7\": \"YYYY_MM_DD_eqe_cell###.csv\",\n",
    "    # Add more weeks as needed\n",
    "}\n",
    "\n",
    "# Color cycle for different weeks\n",
    "colors = ['blue', 'red', 'green', 'purple', 'orange', 'brown']\n",
    "\n",
    "# Load all EQE files\n",
    "eqe_weeks = {}\n",
    "for label, fname in eqe_files.items():\n",
    "    try:\n",
    "        eqe_weeks[label] = pd.read_csv(fname)\n",
    "        print(f\"[ok] Loaded {label}: {fname}\")\n",
    "    except FileNotFoundError:\n",
    "        print(f\"[skip] {label}: {fname} not found\")\n",
    "\n",
    "if not eqe_weeks:\n",
    "    print(\"[warn] No EQE files loaded. Check filenames above.\")\n",
    "else:\n",
    "    fig, axes = plt.subplots(4, 2, figsize=(12, 16))\n",
    "    axes_flat = axes.flatten()\n",
    "\n",
    "    for i in range(8):\n",
    "        ax = axes_flat[i]\n",
    "        pix = i + 1\n",
    "        col_eqe = f\"eqe_pix{pix}\"\n",
    "        col_unc = f\"eqe_unc_pix{pix}\"\n",
    "\n",
    "        for j, (label, df_eqe) in enumerate(eqe_weeks.items()):\n",
    "            color = colors[j % len(colors)]\n",
    "            wl = df_eqe[\"Wavelength (nm)\"].to_numpy()\n",
    "\n",
    "            if col_eqe in df_eqe.columns:\n",
    "                eqe_vals = df_eqe[col_eqe].to_numpy()\n",
    "                ax.plot(wl, eqe_vals, color=color, label=label)\n",
    "\n",
    "                # Add uncertainty band if the column exists\n",
    "                if col_unc in df_eqe.columns:\n",
    "                    eqe_unc = df_eqe[col_unc].to_numpy()\n",
    "                    ax.fill_between(wl,\n",
    "                                    eqe_vals - eqe_unc,\n",
    "                                    eqe_vals + eqe_unc,\n",
    "                                    alpha=0.15, color=color)\n",
    "\n",
    "        ax.set_title(f\"Pixel {pix}\")\n",
    "        ax.set_xlabel(\"Wavelength (nm)\")\n",
    "        ax.set_ylabel(\"EQE\")\n",
    "        ax.legend(fontsize=7)\n",
    "        ax.set_ylim(bottom=0)\n",
    "\n",
    "    plt.suptitle(f\"EQE Comparison Over Time — Cell {cell_id}\", fontsize=14)\n",
    "    plt.tight_layout()\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "KCZSSgIlNLSJ"
   },
   "source": [
    "**Question 4:** Discuss what you observe from looking at the comparison of all of your EQE curves. Considering the uncertainty bands, are the changes in EQE between weeks statistically significant? At which wavelengths is the uncertainty largest relative to the EQE value, and why?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "NhaldreWFciG"
   },
   "source": [
    "## Author Contributions (required)\n",
    "\n",
    "List the name of each team member and please describe briefly how each member contributed to the work in lab this week. (e.g., taking a measurement, updating the Colab notebook, etc.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "wzuOP0_l4LG0"
   },
   "source": [
    "## Use of AI (required)\n",
    "\n",
    "1.   How your team used AI tools: Describe the specific tasks where AI-assisted tools were involved. For example, did AI help with writing, debugging, optimizing, or refining your code? Which components of the analysis did your team use AI on?\n",
    "2.   When your team used AI tools: Specify at what stage(s) of your programming process you used AI. Was it during initial code development, troubleshooting, etc.?\n",
    "3.   Which AI tools your team used: Identify the AI tools or platforms your team consulted (e.g., ChatGPT, GitHub Copilot, etc.)."
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
