{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Unit Handling and Validation\n",
    "\n",
    "TimeDataModel treats **units**, **data types**, and **validation** as first-class concerns.  \n",
    "This notebook covers:\n",
    "\n",
    "1. Setting and inspecting units on `TimeSeries` and `TimeSeriesTable`\n",
    "2. Converting between compatible units with `convert_unit()`\n",
    "3. Automatic unit conversion in arithmetic operations\n",
    "4. Resolving units to pint objects with `pint_unit`\n",
    "5. Validating timestamps and frequency with `validate()`\n",
    "6. Using `DataType`, `TimeSeriesType`, and custom `attributes`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.426286Z",
     "iopub.status.busy": "2026-03-01T13:36:46.426216Z",
     "iopub.status.idle": "2026-03-01T13:36:46.503589Z",
     "shell.execute_reply": "2026-03-01T13:36:46.503194Z"
    }
   },
   "outputs": [
    {
     "ename": "ModuleNotFoundError",
     "evalue": "No module named 'timedatamodel'",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mModuleNotFoundError\u001b[39m                       Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdatetime\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m datetime, timedelta, timezone\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnumpy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnp\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtimedatamodel\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtdm\u001b[39;00m\n\u001b[32m      7\u001b[39m base = datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, tzinfo=timezone.utc)\n\u001b[32m      8\u001b[39m timestamps = [base + timedelta(hours=i) \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m24\u001b[39m)]\n",
      "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'timedatamodel'"
     ]
    }
   ],
   "source": [
    "from datetime import datetime, timedelta, timezone\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "import timedatamodel as tdm\n",
    "\n",
    "base = datetime(2024, 1, 15, tzinfo=timezone.utc)\n",
    "timestamps = [base + timedelta(hours=i) for i in range(24)]\n",
    "rng = np.random.default_rng(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Setting units on a TimeSeries\n",
    "\n",
    "The `unit` parameter is a free-form string. It appears in the repr and is carried through all operations."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.504712Z",
     "iopub.status.busy": "2026-03-01T13:36:46.504638Z",
     "iopub.status.idle": "2026-03-01T13:36:46.512688Z",
     "shell.execute_reply": "2026-03-01T13:36:46.512311Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m wind = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=(\u001b[32m8\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      6\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mwind_speed\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mm/s\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m )\n\u001b[32m      9\u001b[39m wind\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "wind = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(8 + rng.normal(0, 2, 24)).tolist(),\n",
    "    name=\"wind_speed\",\n",
    "    unit=\"m/s\",\n",
    ")\n",
    "wind"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.513686Z",
     "iopub.status.busy": "2026-03-01T13:36:46.513620Z",
     "iopub.status.idle": "2026-03-01T13:36:46.520879Z",
     "shell.execute_reply": "2026-03-01T13:36:46.520436Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'wind' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mUnit: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mwind\u001b[49m.unit\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'wind' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"Unit: {wind.unit}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Converting units with `convert_unit()`\n",
    "\n",
    "`convert_unit()` uses [pint](https://pint.readthedocs.io/) under the hood to convert values.  \n",
    "It returns a **new** `TimeSeries` — the original is unchanged.\n",
    "\n",
    "```bash\n",
    "pip install timedatamodel[pint]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.522000Z",
     "iopub.status.busy": "2026-03-01T13:36:46.521932Z",
     "iopub.status.idle": "2026-03-01T13:36:46.529419Z",
     "shell.execute_reply": "2026-03-01T13:36:46.529037Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'wind' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m wind_kmh = \u001b[43mwind\u001b[49m.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mkm/h\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m wind_knot = wind.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mknot\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mOriginal:  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mwind.unit\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m5s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  mean=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmean(wind.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.2f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'wind' is not defined"
     ]
    }
   ],
   "source": [
    "wind_kmh = wind.convert_unit(\"km/h\")\n",
    "wind_knot = wind.convert_unit(\"knot\")\n",
    "\n",
    "print(f\"Original:  {wind.unit:5s}  mean={np.nanmean(wind.arr):.2f}\")\n",
    "print(f\"Converted: {wind_kmh.unit:5s}  mean={np.nanmean(wind_kmh.arr):.2f}\")\n",
    "print(f\"Converted: {wind_knot.unit:5s}  mean={np.nanmean(wind_knot.arr):.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.530313Z",
     "iopub.status.busy": "2026-03-01T13:36:46.530252Z",
     "iopub.status.idle": "2026-03-01T13:36:46.538444Z",
     "shell.execute_reply": "2026-03-01T13:36:46.538005Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m energy_kwh = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=(\u001b[32m500\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m50\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      6\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33menergy\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mkWh\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m )\n\u001b[32m     10\u001b[39m energy_mwh = energy_kwh.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mMWh\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m     11\u001b[39m energy_j = energy_kwh.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mJ\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "energy_kwh = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(500 + rng.normal(0, 50, 24)).tolist(),\n",
    "    name=\"energy\",\n",
    "    unit=\"kWh\",\n",
    ")\n",
    "\n",
    "energy_mwh = energy_kwh.convert_unit(\"MWh\")\n",
    "energy_j = energy_kwh.convert_unit(\"J\")\n",
    "\n",
    "print(f\"kWh: mean={np.nanmean(energy_kwh.arr):.1f}\")\n",
    "print(f\"MWh: mean={np.nanmean(energy_mwh.arr):.4f}\")\n",
    "print(f\"J:   mean={np.nanmean(energy_j.arr):.0f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Incompatible units raise an error"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.539434Z",
     "iopub.status.busy": "2026-03-01T13:36:46.539370Z",
     "iopub.status.idle": "2026-03-01T13:36:46.546221Z",
     "shell.execute_reply": "2026-03-01T13:36:46.545802Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'wind' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[43mwind\u001b[49m.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m      4\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mError: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'wind' is not defined"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    wind.convert_unit(\"MW\")\n",
    "except ValueError as e:\n",
    "    print(f\"Error: {e}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.547054Z",
     "iopub.status.busy": "2026-03-01T13:36:46.547000Z",
     "iopub.status.idle": "2026-03-01T13:36:46.561142Z",
     "shell.execute_reply": "2026-03-01T13:36:46.560719Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m no_unit = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     timestamps=timestamps,\n\u001b[32m      4\u001b[39m     values=rng.normal(\u001b[32m0\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m24\u001b[39m).tolist(),\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mdimensionless\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m )\n\u001b[32m      8\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m      9\u001b[39m     no_unit.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "no_unit = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=rng.normal(0, 1, 24).tolist(),\n",
    "    name=\"dimensionless\",\n",
    ")\n",
    "\n",
    "try:\n",
    "    no_unit.convert_unit(\"MW\")\n",
    "except ValueError as e:\n",
    "    print(f\"Error: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Automatic unit conversion in arithmetic\n",
    "\n",
    "When you add or subtract two `TimeSeries` with compatible units, values are automatically converted to the left operand's unit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.562181Z",
     "iopub.status.busy": "2026-03-01T13:36:46.562128Z",
     "iopub.status.idle": "2026-03-01T13:36:46.571282Z",
     "shell.execute_reply": "2026-03-01T13:36:46.570872Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m power_mw = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     timestamps=timestamps,\n\u001b[32m      4\u001b[39m     values=(\u001b[32m100\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m10\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mplant_a\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m )\n\u001b[32m      9\u001b[39m power_kw = tdm.TimeSeries(\n\u001b[32m     10\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     11\u001b[39m     timestamps=timestamps,\n\u001b[32m   (...)\u001b[39m\u001b[32m     14\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mkW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     15\u001b[39m )\n\u001b[32m     17\u001b[39m total = power_mw + power_kw\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "power_mw = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(100 + rng.normal(0, 10, 24)).tolist(),\n",
    "    name=\"plant_a\",\n",
    "    unit=\"MW\",\n",
    ")\n",
    "\n",
    "power_kw = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(50000 + rng.normal(0, 5000, 24)).tolist(),\n",
    "    name=\"plant_b\",\n",
    "    unit=\"kW\",\n",
    ")\n",
    "\n",
    "total = power_mw + power_kw\n",
    "print(f\"Result unit: {total.unit}\")\n",
    "print(f\"plant_a mean: {np.nanmean(power_mw.arr):.1f} MW\")\n",
    "print(f\"plant_b mean: {np.nanmean(power_kw.arr):.1f} kW = {np.nanmean(power_kw.arr)/1000:.1f} MW\")\n",
    "print(f\"total mean:   {np.nanmean(total.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mismatched unit *presence* (one has a unit, the other doesn't) raises an error:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.572356Z",
     "iopub.status.busy": "2026-03-01T13:36:46.572285Z",
     "iopub.status.idle": "2026-03-01T13:36:46.579222Z",
     "shell.execute_reply": "2026-03-01T13:36:46.578766Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'power_mw' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     _ = \u001b[43mpower_mw\u001b[49m + no_unit\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m      4\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mError: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'power_mw' is not defined"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    _ = power_mw + no_unit\n",
    "except ValueError as e:\n",
    "    print(f\"Error: {e}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Resolving units with `pint_unit`\n",
    "\n",
    "The `pint_unit` property returns a `pint.Unit` object for programmatic inspection."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.580165Z",
     "iopub.status.busy": "2026-03-01T13:36:46.580109Z",
     "iopub.status.idle": "2026-03-01T13:36:46.586358Z",
     "shell.execute_reply": "2026-03-01T13:36:46.585993Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'power_mw' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m pu = \u001b[43mpower_mw\u001b[49m.pint_unit\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mpint unit: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mpu\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mtype:      \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(pu).\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'power_mw' is not defined"
     ]
    }
   ],
   "source": [
    "pu = power_mw.pint_unit\n",
    "print(f\"pint unit: {pu}\")\n",
    "print(f\"type:      {type(pu).__name__}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Units on TimeSeriesTable\n",
    "\n",
    "`TimeSeriesTable` supports per-column units via the `units` parameter.  \n",
    "`convert_unit()` can target a single column or all columns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.587229Z",
     "iopub.status.busy": "2026-03-01T13:36:46.587178Z",
     "iopub.status.idle": "2026-03-01T13:36:46.594684Z",
     "shell.execute_reply": "2026-03-01T13:36:46.594330Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m table = \u001b[43mtdm\u001b[49m.TimeSeriesTable(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=np.column_stack([\n\u001b[32m      6\u001b[39m         \u001b[32m100\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      7\u001b[39m         \u001b[32m8\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      8\u001b[39m     ]),\n\u001b[32m      9\u001b[39m     names=[\u001b[33m\"\u001b[39m\u001b[33mpower\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mwind_speed\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     10\u001b[39m     units=[\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mm/s\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     11\u001b[39m )\n\u001b[32m     12\u001b[39m table\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "table = tdm.TimeSeriesTable(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=np.column_stack([\n",
    "        100 + rng.normal(0, 15, 24),\n",
    "        8 + rng.normal(0, 2, 24),\n",
    "    ]),\n",
    "    names=[\"power\", \"wind_speed\"],\n",
    "    units=[\"MW\", \"m/s\"],\n",
    ")\n",
    "table"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.595610Z",
     "iopub.status.busy": "2026-03-01T13:36:46.595559Z",
     "iopub.status.idle": "2026-03-01T13:36:46.602472Z",
     "shell.execute_reply": "2026-03-01T13:36:46.602091Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'table' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m table_kw = \u001b[43mtable\u001b[49m.convert_unit(\u001b[33m\"\u001b[39m\u001b[33mkW\u001b[39m\u001b[33m\"\u001b[39m, column=\u001b[33m\"\u001b[39m\u001b[33mpower\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mOriginal units: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtable.units\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mAfter convert:  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtable_kw.units\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'table' is not defined"
     ]
    }
   ],
   "source": [
    "table_kw = table.convert_unit(\"kW\", column=\"power\")\n",
    "\n",
    "print(f\"Original units: {table.units}\")\n",
    "print(f\"After convert:  {table_kw.units}\")\n",
    "print(f\"Power mean: {table.arr[:, 0].mean():.1f} MW → {table_kw.arr[:, 0].mean():.1f} kW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Validating timestamps and frequency\n",
    "\n",
    "`validate()` checks that timestamps are strictly increasing and match the declared frequency.  \n",
    "It returns a list of warning strings — an empty list means everything is consistent."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.603342Z",
     "iopub.status.busy": "2026-03-01T13:36:46.603286Z",
     "iopub.status.idle": "2026-03-01T13:36:46.610357Z",
     "shell.execute_reply": "2026-03-01T13:36:46.610050Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m good = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     timestamps=timestamps,\n\u001b[32m      4\u001b[39m     values=rng.normal(\u001b[32m0\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m24\u001b[39m).tolist(),\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mclean\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m )\n\u001b[32m      8\u001b[39m warnings = good.validate()\n\u001b[32m      9\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mWarnings: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mwarnings\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "good = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=rng.normal(0, 1, 24).tolist(),\n",
    "    name=\"clean\",\n",
    ")\n",
    "\n",
    "warnings = good.validate()\n",
    "print(f\"Warnings: {warnings}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.611323Z",
     "iopub.status.busy": "2026-03-01T13:36:46.611269Z",
     "iopub.status.idle": "2026-03-01T13:36:46.618588Z",
     "shell.execute_reply": "2026-03-01T13:36:46.618249Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'timestamps' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m gap_timestamps = \u001b[43mtimestamps\u001b[49m[:\u001b[32m12\u001b[39m] + timestamps[\u001b[32m14\u001b[39m:]\n\u001b[32m      2\u001b[39m gap_values = rng.normal(\u001b[32m0\u001b[39m, \u001b[32m1\u001b[39m, \u001b[38;5;28mlen\u001b[39m(gap_timestamps)).tolist()\n\u001b[32m      4\u001b[39m gapped = tdm.TimeSeries(\n\u001b[32m      5\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     timestamps=gap_timestamps,\n\u001b[32m      7\u001b[39m     values=gap_values,\n\u001b[32m      8\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mhas_gap\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m )\n",
      "\u001b[31mNameError\u001b[39m: name 'timestamps' is not defined"
     ]
    }
   ],
   "source": [
    "gap_timestamps = timestamps[:12] + timestamps[14:]\n",
    "gap_values = rng.normal(0, 1, len(gap_timestamps)).tolist()\n",
    "\n",
    "gapped = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=gap_timestamps,\n",
    "    values=gap_values,\n",
    "    name=\"has_gap\",\n",
    ")\n",
    "\n",
    "for w in gapped.validate():\n",
    "    print(f\"⚠ {w}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.619646Z",
     "iopub.status.busy": "2026-03-01T13:36:46.619582Z",
     "iopub.status.idle": "2026-03-01T13:36:46.627354Z",
     "shell.execute_reply": "2026-03-01T13:36:46.627032Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'timestamps' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[15]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m bad_order = \u001b[43mtimestamps\u001b[49m[:\u001b[32m12\u001b[39m] + [timestamps[\u001b[32m13\u001b[39m], timestamps[\u001b[32m12\u001b[39m]] + timestamps[\u001b[32m14\u001b[39m:]\n\u001b[32m      2\u001b[39m bad_values = rng.normal(\u001b[32m0\u001b[39m, \u001b[32m1\u001b[39m, \u001b[38;5;28mlen\u001b[39m(bad_order)).tolist()\n\u001b[32m      4\u001b[39m unordered = tdm.TimeSeries(\n\u001b[32m      5\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     timestamps=bad_order,\n\u001b[32m      7\u001b[39m     values=bad_values,\n\u001b[32m      8\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33munordered\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m )\n",
      "\u001b[31mNameError\u001b[39m: name 'timestamps' is not defined"
     ]
    }
   ],
   "source": [
    "bad_order = timestamps[:12] + [timestamps[13], timestamps[12]] + timestamps[14:]\n",
    "bad_values = rng.normal(0, 1, len(bad_order)).tolist()\n",
    "\n",
    "unordered = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=bad_order,\n",
    "    values=bad_values,\n",
    "    name=\"unordered\",\n",
    ")\n",
    "\n",
    "for w in unordered.validate():\n",
    "    print(f\"⚠ {w}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Detecting missing values\n",
    "\n",
    "The `has_missing` property returns `True` when any value is `None` (NaN)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.628484Z",
     "iopub.status.busy": "2026-03-01T13:36:46.628426Z",
     "iopub.status.idle": "2026-03-01T13:36:46.635857Z",
     "shell.execute_reply": "2026-03-01T13:36:46.635531Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'rng' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[16]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m values_with_gaps = \u001b[43mrng\u001b[49m.normal(\u001b[32m100\u001b[39m, \u001b[32m10\u001b[39m, \u001b[32m24\u001b[39m).tolist()\n\u001b[32m      2\u001b[39m values_with_gaps[\u001b[32m5\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m      3\u001b[39m values_with_gaps[\u001b[32m18\u001b[39m] = \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "\u001b[31mNameError\u001b[39m: name 'rng' is not defined"
     ]
    }
   ],
   "source": [
    "values_with_gaps = rng.normal(100, 10, 24).tolist()\n",
    "values_with_gaps[5] = None\n",
    "values_with_gaps[18] = None\n",
    "\n",
    "sparse = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=values_with_gaps,\n",
    "    name=\"sparse\",\n",
    "    unit=\"MW\",\n",
    ")\n",
    "\n",
    "print(f\"has_missing: {sparse.has_missing}\")\n",
    "print(f\"NaN count:   {np.isnan(sparse.arr).sum()}\")\n",
    "print(f\"Length:      {len(sparse)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## DataType — classifying your data\n",
    "\n",
    "The `DataType` enum communicates what kind of data a series holds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.636800Z",
     "iopub.status.busy": "2026-03-01T13:36:46.636743Z",
     "iopub.status.idle": "2026-03-01T13:36:46.643578Z",
     "shell.execute_reply": "2026-03-01T13:36:46.643101Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Available DataType values:\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mAvailable DataType values:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m dt \u001b[38;5;129;01min\u001b[39;00m \u001b[43mtdm\u001b[49m.DataType:\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdt.value\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "print(\"Available DataType values:\")\n",
    "for dt in tdm.DataType:\n",
    "    print(f\"  {dt.value}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.644391Z",
     "iopub.status.busy": "2026-03-01T13:36:46.644330Z",
     "iopub.status.idle": "2026-03-01T13:36:46.652137Z",
     "shell.execute_reply": "2026-03-01T13:36:46.651840Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[18]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m measured = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     timestamps=timestamps,\n\u001b[32m      4\u001b[39m     values=(\u001b[32m100\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m10\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mwind_measured\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     data_type=tdm.DataType.MEASUREMENT,\n\u001b[32m      8\u001b[39m )\n\u001b[32m     10\u001b[39m forecast = tdm.TimeSeries(\n\u001b[32m     11\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     12\u001b[39m     timestamps=timestamps,\n\u001b[32m   (...)\u001b[39m\u001b[32m     16\u001b[39m     data_type=tdm.DataType.FORECAST,\n\u001b[32m     17\u001b[39m )\n\u001b[32m     19\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmeasured.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m: data_type=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmeasured.data_type\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "measured = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(100 + rng.normal(0, 10, 24)).tolist(),\n",
    "    name=\"wind_measured\",\n",
    "    unit=\"MW\",\n",
    "    data_type=tdm.DataType.MEASUREMENT,\n",
    ")\n",
    "\n",
    "forecast = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(105 + rng.normal(0, 15, 24)).tolist(),\n",
    "    name=\"wind_forecast\",\n",
    "    unit=\"MW\",\n",
    "    data_type=tdm.DataType.FORECAST,\n",
    ")\n",
    "\n",
    "print(f\"{measured.name}: data_type={measured.data_type}\")\n",
    "print(f\"{forecast.name}: data_type={forecast.data_type}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## TimeSeriesType — structural classification\n",
    "\n",
    "`TimeSeriesType` describes the structural nature of the series."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.652986Z",
     "iopub.status.busy": "2026-03-01T13:36:46.652937Z",
     "iopub.status.idle": "2026-03-01T13:36:46.659579Z",
     "shell.execute_reply": "2026-03-01T13:36:46.659214Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Available TimeSeriesType values:\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[19]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mAvailable TimeSeriesType values:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m tst \u001b[38;5;129;01min\u001b[39;00m \u001b[43mtdm\u001b[49m.TimeSeriesType:\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtst.value\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "print(\"Available TimeSeriesType values:\")\n",
    "for tst in tdm.TimeSeriesType:\n",
    "    print(f\"  {tst.value}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.660475Z",
     "iopub.status.busy": "2026-03-01T13:36:46.660425Z",
     "iopub.status.idle": "2026-03-01T13:36:46.667154Z",
     "shell.execute_reply": "2026-03-01T13:36:46.666807Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[20]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m flat = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H, timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     timestamps=timestamps,\n\u001b[32m      4\u001b[39m     values=rng.normal(\u001b[32m0\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m24\u001b[39m).tolist(),\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mflat_series\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     timeseries_type=tdm.TimeSeriesType.FLAT,\n\u001b[32m      7\u001b[39m )\n\u001b[32m      8\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mtimeseries_type: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mflat.timeseries_type\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "flat = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H, timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=rng.normal(0, 1, 24).tolist(),\n",
    "    name=\"flat_series\",\n",
    "    timeseries_type=tdm.TimeSeriesType.FLAT,\n",
    ")\n",
    "print(f\"timeseries_type: {flat.timeseries_type}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Custom attributes\n",
    "\n",
    "The `attributes` dict stores arbitrary key-value metadata — source system, fuel type, model version, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.668055Z",
     "iopub.status.busy": "2026-03-01T13:36:46.667997Z",
     "iopub.status.idle": "2026-03-01T13:36:46.675911Z",
     "shell.execute_reply": "2026-03-01T13:36:46.675412Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[21]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m rich = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=(\u001b[32m80\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m10\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      6\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mwind_farm_alpha\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m     description=\u001b[33m\"\u001b[39m\u001b[33mMeasured output from Wind Farm Alpha\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m     data_type=tdm.DataType.MEASUREMENT,\n\u001b[32m     10\u001b[39m     timeseries_type=tdm.TimeSeriesType.FLAT,\n\u001b[32m     11\u001b[39m     attributes={\n\u001b[32m     12\u001b[39m         \u001b[33m\"\u001b[39m\u001b[33msource\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mSCADA\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     13\u001b[39m         \u001b[33m\"\u001b[39m\u001b[33mfuel\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mwind\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     14\u001b[39m         \u001b[33m\"\u001b[39m\u001b[33mcapacity_mw\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33m120\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     15\u001b[39m         \u001b[33m\"\u001b[39m\u001b[33moperator\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33mNorthWind Energy\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     16\u001b[39m     },\n\u001b[32m     17\u001b[39m )\n\u001b[32m     19\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mAttributes: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrich.attributes\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m     20\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCapacity:   \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrich.attributes[\u001b[33m'\u001b[39m\u001b[33mcapacity_mw\u001b[39m\u001b[33m'\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "rich = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=(80 + rng.normal(0, 10, 24)).tolist(),\n",
    "    name=\"wind_farm_alpha\",\n",
    "    unit=\"MW\",\n",
    "    description=\"Measured output from Wind Farm Alpha\",\n",
    "    data_type=tdm.DataType.MEASUREMENT,\n",
    "    timeseries_type=tdm.TimeSeriesType.FLAT,\n",
    "    attributes={\n",
    "        \"source\": \"SCADA\",\n",
    "        \"fuel\": \"wind\",\n",
    "        \"capacity_mw\": \"120\",\n",
    "        \"operator\": \"NorthWind Energy\",\n",
    "    },\n",
    ")\n",
    "\n",
    "print(f\"Attributes: {rich.attributes}\")\n",
    "print(f\"Capacity:   {rich.attributes['capacity_mw']} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Frequency enum\n",
    "\n",
    "`Frequency` is a `StrEnum` with helpers for calendar-based vs fixed-duration frequencies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.676789Z",
     "iopub.status.busy": "2026-03-01T13:36:46.676733Z",
     "iopub.status.idle": "2026-03-01T13:36:46.683639Z",
     "shell.execute_reply": "2026-03-01T13:36:46.683261Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Frequency  timedelta               calendar?\n",
      "---------------------------------------------\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[22]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33mFrequency\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m<8s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33mtimedelta\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m<22s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33mcalendar?\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33m-\u001b[39m\u001b[33m\"\u001b[39m * \u001b[32m45\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m f \u001b[38;5;129;01min\u001b[39;00m \u001b[43mtdm\u001b[49m.Frequency:\n\u001b[32m      4\u001b[39m     td = f.to_timedelta()\n\u001b[32m      5\u001b[39m     td_str = \u001b[38;5;28mstr\u001b[39m(td) \u001b[38;5;28;01mif\u001b[39;00m td \u001b[38;5;28;01melse\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33m-\u001b[39m\u001b[33m\"\u001b[39m\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"{'Frequency':<8s}  {'timedelta':<22s}  {'calendar?'}\")\n",
    "print(\"-\" * 45)\n",
    "for f in tdm.Frequency:\n",
    "    td = f.to_timedelta()\n",
    "    td_str = str(td) if td else \"-\"\n",
    "    print(f\"{f.value:<8s}  {td_str:<22s}  {f.is_calendar_based}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Metadata survives serialization\n",
    "\n",
    "Units, data types, attributes, and other metadata round-trip through JSON."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:46.684548Z",
     "iopub.status.busy": "2026-03-01T13:36:46.684496Z",
     "iopub.status.idle": "2026-03-01T13:36:46.691089Z",
     "shell.execute_reply": "2026-03-01T13:36:46.690831Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'rich' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[23]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m json_str = \u001b[43mrich\u001b[49m.to_json()\n\u001b[32m      2\u001b[39m restored = tdm.TimeSeries.from_json(json_str)\n\u001b[32m      4\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33munit:            \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mrestored.unit\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'rich' is not defined"
     ]
    }
   ],
   "source": [
    "json_str = rich.to_json()\n",
    "restored = tdm.TimeSeries.from_json(json_str)\n",
    "\n",
    "print(f\"unit:            {restored.unit}\")\n",
    "print(f\"data_type:       {restored.data_type}\")\n",
    "print(f\"timeseries_type: {restored.timeseries_type}\")\n",
    "print(f\"attributes:      {restored.attributes}\")\n",
    "print(f\"description:     {restored.description}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "| Feature | API |\n",
    "| --- | --- |\n",
    "| Set unit | `TimeSeries(..., unit=\"MW\")` |\n",
    "| Convert unit | `ts.convert_unit(\"kW\")` — returns new series |\n",
    "| Auto-convert in arithmetic | `ts_mw + ts_kw` converts to left operand's unit |\n",
    "| Pint integration | `ts.pint_unit` — resolves to `pint.Unit` |\n",
    "| Per-column units | `TimeSeriesTable(..., units=[\"MW\", \"m/s\"])` |\n",
    "| Column conversion | `table.convert_unit(\"kW\", column=\"power\")` |\n",
    "| Validate timestamps | `ts.validate()` → list of warning strings |\n",
    "| Missing values | `ts.has_missing` |\n",
    "| Data classification | `DataType.MEASUREMENT`, `.FORECAST`, `.SCENARIO`, … |\n",
    "| Structural type | `TimeSeriesType.FLAT`, `.OVERLAPPING` |\n",
    "| Custom metadata | `attributes={\"key\": \"value\"}` |\n",
    "| Frequency info | `Frequency.PT1H.to_timedelta()`, `.is_calendar_based` |\n",
    "\n",
    "Next up: **nb_04** covers arithmetic operations and comparisons on TimeSeries."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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.14.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
