{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "3530dac3",
   "metadata": {},
   "source": [
    "# PVT Calculator — API usage example\n",
    "\n",
    "This notebook shows how to call the pengtools **PVT calculator** over the HTTP API:\n",
    "\n",
    "1. **Configuration** — `PENGTOOLS_API_BASE` and `PENGTOOLS_API_KEY`.\n",
    "2. **Helpers** — a `requests` session with the Bearer token and parsing of the `{success, data}` envelope.\n",
    "3. **Sample model** — a small, valid oil model in the `unit_system: \"RU\"` shortcut units.\n",
    "3. **Synchronous calculation** — `POST /pvt-calculator/calc`.\n",
    "4. **Plots** — render every curve from the `plots` block, one after another.\n",
    "5. **Error handling** — 401 / 422\n",
    "\n",
    "> Create a key in the accounts portal: **accounts.pengtools.com → profile → API keys**.\n",
    "> Contact us at support@pengtools.com to activate your API trial license.\n",
    "> The notebook is self-contained , so you can run it from anywhere.\n",
    "> Each pengtools tool gets its own notebook; this one covers PVT.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "091adc93",
   "metadata": {},
   "source": [
    "## 0. Install dependencies\n",
    "\n",
    "Uncomment and run if needed. `requests` is required; `matplotlib` is only needed for the plots section.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f3c73cf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install \"requests>=2.25,<3\" matplotlib\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52ab0a8f",
   "metadata": {},
   "source": [
    "## 1. Configuration\n",
    "\n",
    "Values are read from environment variables; if unset, use the placeholders here.\n",
    "**Do not commit a real key into the notebook.**\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef991569",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "# API base URL. Defaults to production; for local development point it at your own,\n",
    "# e.g. \"http://localhost:8080\" or \"https://api.pengtools.com\".\n",
    "PENGTOOLS_API_BASE = os.environ.get(\"PENGTOOLS_API_BASE\", \"https://api.pengtools.com\").rstrip(\"/\")\n",
    "\n",
    "# Bearer token from the accounts portal. Prefer setting it via the environment:\n",
    "#   export PENGTOOLS_API_KEY=\"<token>\"\n",
    "PENGTOOLS_API_KEY = os.environ.get(\"PENGTOOLS_API_KEY\", \"\")  # <-- or paste the token here temporarily\n",
    "\n",
    "if not PENGTOOLS_API_KEY:\n",
    "    print(\"PENGTOOLS_API_KEY is not set. Create a key in accounts.pengtools.com -> profile -> API keys\\n\"\n",
    "          \"and either `export PENGTOOLS_API_KEY=<token>` or paste the value into the cell above.\")\n",
    "else:\n",
    "    print(\"API base:\", PENGTOOLS_API_BASE)\n",
    "    print(\"API key:  ***\" + PENGTOOLS_API_KEY[-4:])\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd3ab930",
   "metadata": {},
   "source": [
    "## 2. Helpers\n",
    "\n",
    "`make_session()` returns a `requests.Session` with the headers already set. `unwrap()` pulls `data`\n",
    "out of the `{success, data}` envelope and raises `RuntimeError` with the parsed errors on a non-2xx response.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e04c5145",
   "metadata": {},
   "outputs": [],
   "source": [
    "import requests\n",
    "\n",
    "\n",
    "def make_session():\n",
    "    \"\"\"A requests.Session pre-loaded with the Bearer token and JSON headers.\"\"\"\n",
    "    s = requests.Session()\n",
    "    s.headers.update({\n",
    "        \"Authorization\": \"Bearer \" + PENGTOOLS_API_KEY,\n",
    "        \"Content-Type\": \"application/json\",\n",
    "        \"Accept\": \"application/json\",\n",
    "    })\n",
    "    return s\n",
    "\n",
    "\n",
    "def unwrap(resp):\n",
    "    \"\"\"Return the `data` payload from the {success, data} envelope.\n",
    "\n",
    "    Raises RuntimeError with the parsed errors on a non-2xx response.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        body = resp.json()\n",
    "    except ValueError:\n",
    "        resp.raise_for_status()\n",
    "        raise RuntimeError(\"Response was not JSON: \" + resp.text[:200])\n",
    "\n",
    "    data = body.get(\"data\", body)\n",
    "    if not resp.ok:\n",
    "        raise RuntimeError(\"HTTP {0}: {1}\".format(resp.status_code, data.get(\"errors\", data)))\n",
    "    return data\n",
    "\n",
    "\n",
    "session = make_session()\n",
    "print(\"session ready\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a751f138",
   "metadata": {},
   "source": [
    "## 3. Sample model\n",
    "\n",
    "A small, valid oil model in the `unit_system: \"RU\"` shortcut units\n",
    "(ATM / degC / m3.m-3 / specific gravity); the bubble point is derived from the solution GOR\n",
    "(`isRsbUsedForPb = true`).\n",
    "\n",
    "Both endpoints accept the **same body**: the asynchronous endpoint expands `unit_system`\n",
    "when the job is created, so sync and async return identical results for identical input.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "80e4a565",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Used by both the synchronous and the asynchronous sections — in RU units.\n",
    "SAMPLE_OIL_BODY_RU = {\n",
    "    \"unit_system\": \"RU\",\n",
    "    \"defaultFluidType\": \"OIL\",\n",
    "    \"maximumPressure\": 300,\n",
    "    \"reservoirPressure\": 300,\n",
    "    \"temperature\": 90,\n",
    "    \"pBubbleInitial\": 200,\n",
    "    \"rsbInitial\": 60,\n",
    "    \"isRsbUsedForPb\": True,\n",
    "    \"sgOil\": 0.85,\n",
    "    \"sgGas\": 0.75,\n",
    "    \"sgWater\": 1.0,\n",
    "}\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e2f01fb",
   "metadata": {},
   "source": [
    "## 4. Synchronous calculation\n",
    "\n",
    "`POST /pvt-calculator/calc` returns the result immediately. The `solution.OIL` block holds scalar\n",
    "values, and the `plots.OIL` block holds curves as lists of `[pressure, value]` pairs.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4c780df6",
   "metadata": {},
   "outputs": [],
   "source": [
    "url = PENGTOOLS_API_BASE + \"/pvt-calculator/calc\"\n",
    "\n",
    "resp = session.post(url, json=SAMPLE_OIL_BODY_RU)\n",
    "data = unwrap(resp)  # raises on non-2xx with the parsed errors\n",
    "\n",
    "oil = data[\"solution\"][\"OIL\"]\n",
    "print(\"Bubble point (ATM):   {0}\".format(oil[\"solution_pBubble\"]))\n",
    "print(\"Solution GOR (m3/m3): {0}\".format(oil[\"solution_rsb\"]))\n",
    "print(\"Oil density (kg/m3):  {0}\".format(oil[\"solution_density\"]))\n",
    "print(\"Oil FVF (m3/m3):      {0}\".format(oil[\"solution_fvf\"]))\n",
    "\n",
    "rs_curve = data[\"plots\"][\"OIL\"][\"Rs\"]\n",
    "print(\"\\nRs curve (first 3 of {0} points): [pressure_ATM, Rs_m3/m3]\".format(len(rs_curve)))\n",
    "for pressure, rs in rs_curve[:3]:\n",
    "    print(\"  {0:>10.3f}  {1:>10.4f}\".format(pressure, rs))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ffce3b3",
   "metadata": {},
   "source": [
    "## 5. Plot the curves\n",
    "\n",
    "The `plots.OIL` block contains one or more curves (e.g. `Rs`, `Bo`, ...). We loop over every curve\n",
    "and render each one as its own figure, one after another. The cell reuses `data` from the\n",
    "synchronous calculation above.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "abaa8f9f",
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "curves = data[\"plots\"][\"OIL\"]\n",
    "\n",
    "for name, curve in curves.items():\n",
    "    xs = [p for p, _ in curve]\n",
    "    ys = [v for _, v in curve]\n",
    "\n",
    "    plt.figure(figsize=(7, 4))\n",
    "    plt.plot(xs, ys)\n",
    "    plt.title(\"{0} vs pressure\".format(name))\n",
    "    plt.xlabel(\"Pressure, ATM\")\n",
    "    plt.ylabel(name)\n",
    "    plt.grid(True, alpha=0.3)\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3001f89b",
   "metadata": {},
   "source": [
    "## 6. Error handling\n",
    "\n",
    "The API example signals those error classes:\n",
    "\n",
    "| Code | Meaning | Client reaction |\n",
    "|------|---------|-----------------|\n",
    "| **401** | missing / invalid Bearer key | check the token |\n",
    "| **422** | validation / calculation error (errors keyed by attribute, or a flat list) | fix the input |\n",
    "\n",
    "The demos below intentionally trigger errors.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09cd1fbe",
   "metadata": {},
   "outputs": [],
   "source": [
    "import copy\n",
    "\n",
    "\n",
    "def demo_401():\n",
    "    \"\"\"No Authorization header -> 401.\"\"\"\n",
    "    resp = requests.post(\n",
    "        PENGTOOLS_API_BASE + \"/pvt-calculator/calc\",\n",
    "        json=SAMPLE_OIL_BODY_RU,\n",
    "        headers={\"Content-Type\": \"application/json\"},\n",
    "    )\n",
    "    print(\"[401 demo] status={0} (expected 401)\".format(resp.status_code))\n",
    "\n",
    "\n",
    "def demo_422():\n",
    "    \"\"\"Out-of-range temperature -> 422 with field errors.\"\"\"\n",
    "    bad = copy.deepcopy(SAMPLE_OIL_BODY_RU)\n",
    "    bad[\"temperature\"] = -100  # below the allowed band\n",
    "    resp = session.post(PENGTOOLS_API_BASE + \"/pvt-calculator/calc\", json=bad)\n",
    "    print(\"[422 demo] status={0} (expected 422)\".format(resp.status_code))\n",
    "    if resp.status_code == 422:\n",
    "        errors = resp.json().get(\"data\", {}).get(\"errors\", {})\n",
    "        print(\"           errors: {0}\".format(errors))\n",
    "\n",
    "\n",
    "demo_401()\n",
    "demo_422()\n"
   ]
  }
 ],
 "metadata": {
  "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.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
