{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# CQD hypothesis laboratory\n",
        "\n",
        "A portable, reproducible simulator for asking **which assumptions change\n",
        "the prediction, and which measurements could distinguish mechanisms?**\n",
        "\n",
        "**All results here are synthetic and uncalibrated.** Numerical checks\n",
        "establish that the equations are solved consistently; they do not\n",
        "establish accuracy for a manufactured CQD batch. Prior envelopes are\n",
        "chosen scenarios, not experimental confidence intervals.\n",
        "\n",
        "**Colab:** upload this notebook at https://colab.research.google.com/,\n",
        "choose **Runtime \u2192 Run all**, then edit the form in section 1 and rerun\n",
        "the following cells. CPU runtime is sufficient. A snapshot of the two\n",
        "model modules is embedded, so cloning or publishing a repository is\n",
        "unnecessary. Dependencies are installed only when missing.\n",
        "\n",
        "**Local:** use the project environment as a Jupyter kernel. The notebook\n",
        "uses its bundled snapshot in both environments. After editing the Python\n",
        "modules, run `python make_simulator_notebook.py` to update that snapshot."
      ],
      "id": "cqd-lab-00"
    },
    {
      "cell_type": "code",
      "metadata": {
        "cellView": "form"
      },
      "source": [
        "# @title Load the bundled simulator (no repository download)\n",
        "import base64, hashlib, importlib.util, io, json, sys, tempfile, zipfile\n",
        "from pathlib import Path\n",
        "import subprocess\n",
        "\n",
        "dependencies = {\"numpy\": \"numpy>=1.24\", \"pandas\": \"pandas>=2.0\",\n",
        "                \"scipy\": \"scipy>=1.10\", \"matplotlib\": \"matplotlib>=3.7\"}\n",
        "missing = [requirement for name, requirement in dependencies.items()\n",
        "           if importlib.util.find_spec(name) is None]\n",
        "if missing:\n",
        "    subprocess.check_call([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", *missing])\n",
        "\n",
        "BUNDLED_SHA256 = {'fe3_cqd_sensor.py': '0174e84097db884b30d36b6c74bfdf94b3657f20df49eea81ea27f38ac0cdf9d', 'cqd_simulator.py': '14a5b24e99c3d5f0f3ef0507f3e1fb3c371344f726ad63970cee98e65b5d0382'}\n",
        "BUNDLED_SOURCE_ZIP = (\n",
        "    \"UEsDBBQAAAAIAAAAIQC4SZApESQAAOFxAAARAAAAZmUzX2NxZF9zZW5zb3IucHm1Pf1z20auv+uv2FPnXkhHoiXZTlK3yoxrO7Un/krs9qZPT6Up\"\n",
        "    \"cSXzTJEKSdnW5eX+9gdgP0lRinvj52ljkruLBbBYLIDFrpvNZuMD33nNDj8dsWue5GnGztOQx41+3U+jcc7Hd0ES5UU0brF5Ol/EQRGlSXsU5Dxk\"\n",
        "    \"M2zK0gkjmDnAi5Ipe4yKOzYOslGasC+LICkWMxamRd5woNfcZQ9RwMbpbBQlACIvAOCYwUsIbVssXCbBDD44V8c3LjTnyfiOCoIkZFGS8Kw9ieKC\"\n",
        "    \"Zw0+mfBxkTPn9MOx2xK9zk/aIZ/zJORJwfJFNgnGnI3v+AwoyJZeo3FzF+UM/ivuOENkY/iVLjKoBVQU2QJgTIAnWEzEeewGHpO04KM0vSckoKxx\"\n",
        "    \"e5vxeZaGizH3J9F0kfHcmy9vb1k+zqJ5wUYpAIpm8zQr2CRLZ+yOZxy6/8iXAi7yCaHmjfbqT2OLfVKEs0XOcxaw+d0yj8ZBHC+Bd8DnvEASq2x8\"\n",
        "    \"rfkHVMz2G4x+PnS2P7A+c7pQDo3H2x9Dl22J94/5AzwOPg1dqBx53GMftj90oPbEB76ybTbY0Ay/+rNzd+hB48sEcANBYONFlgFy8HYXAOoJQ0DB\"\n",
        "    \"CGRtjrIjGQstkLUScRjQWQVBOUpnQTKdLaJMiQhWKLJotEBYP9FoME01gYGG4zjIiV3sGj4l7d/TeMYzGsycsyCD0V3ERTSPIx62cCDYLHriIbIg\"\n",
        "    \"KVJgt5QNgdbjXTQG2cr4Q5QucqBLDn0IFROYDGpsUNrDDOkBQHH62EZagBMZzRkGQjKHoeOuBwP8QUgNih1gFhXMObs8arGzy08tBtLBg4xlQDlv\"\n",
        "    \"iWlVRA9RsXQJdRj0+aKA3kmyAuhrEhX4DihEI9kZDMIDBxDAHxRnGAXgCM+gGojh8QPPlgWJFzArXyZQC7D22CHgV8CMzamjnBcMuBFHwIagAHTb\"\n",
        "    \"8zhY5NEo5gK5nMEwNLDqxeWNwoL4N+MBTD94C4MCVEUBA5XxoGDpogDcAXzOQp5H04RNF1EYAJtoGBrA5DAaIwV5iy2SIoo1VcjuaRABgsBJYHYR\"\n",
        "    \"KcZiH16jCbqtQSzx/ckC0fV9NQeDBIBT5VzWKZZzIl+UH0GfLXazmMe80ZDfksVsvkREk7n6NIf5HxDy87DR+IH9GqcjwOTzxa+kNZROiEYR8Gzp\"\n",
        "    \"sd9A2GbBPfezZOq4VAeE2Cgo5Mks9xpQDDMumXvA1TCdeSGfBCCf1Gy35zYaDfhiIOWch/soqdAIiln7vdX2V57gaKWZmP3Alc8cmJHgRLQ7R6Tz\"\n",
        "    \"FLkb8wxGI01eAYEgIYtsRBNvKogTSHrIXYSXCWD1uCJiClvQiwkIjvMQxAsc3GDG9xFYi221YLYl0Wwx61+kCcd1hQSc9z8EMUwPjfjvMPIwthyH\"\n",
        "    \"AubIGPAn4Rlx4CRHJJcMSOGWvmYc5iCNs8Y4yLJgKdgb5PSicApBCHh/EqdB4VLVaEK6AGvGsQO/olxSQe1cV2lU4EMQweD+joCOsyzNnEnzK9L4\"\n",
        "    \"DTRLXpCSAlllKSpFAYNRr3lTd6XIplUF+0yWoh/2c591nt/XiGtQBrjkMM5vJAkZXdOPqvaXOnvfZ19lu2+yQykVBFWNv8LJB0HlU56tSILsFJCN\"\n",
        "    \"8oj0zliLiwPLbdxCbPHBd10GcwcJqakK8Kmm7EfUpUIgsPtXKAvMmEhgTS3OJV2eO/iaSwroGSRMiTy9t1iTfjeNtHf06FCJB+vZjP2tz7qIsPiU\"\n",
        "    \"R//irA/DvwFtAddCGtYgPpsXS5A23gaYuGCkCcxdGo/yEFFbRVMAlZYo3eK3f8+XlZGxSgT7E3ZwcXD2x83x9UbG/pbcJ+ljotqzrxagv2XffgKj\"\n",
        "    \"LE2hEenir69a7JX3zzRKHAXcrUiW+j6w4AwVGQ9ST/jGQHXCyT4oae8IloYPGQyyISmceMStTRxG89gAE6xG8oHd1FYiN07jxSzBocdBSe/8SRaM\"\n",
        "    \"YcCb1mNy19PPaIfi0PjJrNnS3cufZuKHKS5KWBPoAuOWQD0hbdAIX0ZZNL0rEp4rNUIAafUYCGRokZGPUaIxJJmjj3IUw8nQqAqCsXE8KwwBraJ6\"\n",
        "    \"lj3s26Moy/Qg1uJk+hOK0ZpA4URSM2zJ2tYkstYLCRPmS4m1roZsyP5bpY6tDGX/71nX0oPrNIaAZ3QG6Opuc2X5AKhAhD1eQ69IfbIpwBJ4v1m/\"\n",
        "    \"18qfWlLAiorBuipwtqNfJT0fpbhKIoJGSq1n91d/AM7h5cX1zcHFzfWLwQRA84+BGn4UEuWyTbN0MQfXzjI9yeN0G1cfD0BQvgoT4fDy8qS5z3a9\"\n",
        "    \"dy14BRTR6XxaxmAqBOMoFJWoSrfjdbAOVDpZhhlWEqUXJztQ/KPXk6UHs1maRLBwohb/52KK5gfCQkNdYQeuRsLdxrdG4+yi2xGmRZxOHezERarI\"\n",
        "    \"HZ6lMRjxyu9B8x1kDtQeGFvO+Z/tLhvP4F+3VWtfe43jq+vTs8sL/8Pxjn94cHb6y+eDm9PLC0P8zps9kON9trPXkcT9gLo7xTl5F2RT3obFKskn\"\n",
        "    \"PLOcL9F0tyOa9vYMWx55cA+uubDuqw3kdx8EMCZuinY/MOi7/abTYQlY1FCGPHlBkeu67PfTzze/HZxRwOKXg5vDE3b9x8XNyfH16QvKIa4ieTAD\"\n",
        "    \"698ffwmdRBnX3V6n0yGDdZyCTxujE7LP0CKBwpsMrI+qEp9q67vOJGf/KwyxPv0iw91eo7ThKxvg2q6dM1bWCeDFYeWrABuC/OT0asUQ8DVhRIlG\"\n",
        "    \"8mIxG4E4gChjJMaTa5iijAnSdO3TiaCR5fMewyUM+20xvSCRChULFfxCJxVjE+2HIFsKnRRoUDkIJHSA3QBjm4KoHFYR+J7fkbJiMLWARS0mvG0o\"\n",
        "    \"m8D0mYGhr6FoP5v8WRowXIE4erPok6P3srSdnBh8MKCC3AozVLJmxIWXaztFiyTCwAlqHj4NxktQ8HcBev2ZK7ilx5fVDjBwh2ZNYLiIfha4xIuc\"\n",
        "    \"e+xI+Eo5fpAuyyLmbelp3d6CD3V76ykpoN+rZnSCRoVccXDhNyjB+mO9WIY/B/oZABcyE0ZCZLCtJyl2ujihhZZM3IaxU7XYa4J+YGdiGM0ogqoM\"\n",
        "    \"I+GKZ4BqbgtVTRTOQKLhZ/9mHQY2Qxy6zPPAGnbo87YUtAfuetAlKLSMwogMOqcoAywSjxasBQxhxmLQfzzbvksLpFAjluNcwy7MQjmGJYbkB0Ch\"\n",
        "    \"2W3wEliRWh/H0dwpTfKpN+JF4PSQUz3BLvaadbzuHsbGNG+3iZmsDSV7bllNEJOR3/qrMVhwqpmOO96PAgKAFki9hv4TGK8gdgAIQHqDCODTDv7z\"\n",
        "    \"454BZeapDfDtc2DR6zsDSlmmFiQjObJ6d6/UumthInWEjcYO8Ww9Ll0LWNeTjhOK8X6FVTYekgUkwTWor0G5lmErdd+VqmqKKvW69vz5gV1L2QeJ\"\n",
        "    \"g0mRg+riQjNqSyLjAXP+raTmz55UM7LcFw37Rq62tnrAs92O64EFuJxzdH5R/nqdhtTm6R2hNYqSdBYBO0ugWtYQdHtiDHAQNOlAKlDhCkqfDarz\"\n",
        "    \"jkDtrAcFXtBzYe0atNTgaVBCf00AlL1uOl/1wDQVo3wycNSbmWpNEBsogH+tb9rz2jddmlLjge1b1FldCl9tX0qF3ZdNJPZaItrUI99xnCAMfLT7\"\n",
        "    \"Vt9LX9GnFJ/hSXz/ppkzsFzRIXBKf8EWQ1BM+KGM2VA3rTZ8ZjPt5Kp2CsN1DW3fPpxIRz4IQx/XT5gnvlmkV135tXbTQVEEYDtYfh7ZKMYl0v4w\"\n",
        "    \"zBsGxvYYgGENjYmOGiLStr+IhFnvZk3AirbQgctKBIOYyUctXfLdyJNgoKs7tN1F6M+8IjueluVOrR6kBK7Cb62iZ3Vp8//ljPaeyy6vbk7BZ2FX\"\n",
        "    \"ny+vjj/fnB6/sK1uD0QI/5AtloQU7iJztfShaqPrOV5uptlmf1bBdfmqJe1YYsAegwce82QKSt1JZq5Q73LXE/d/JuAqzshawsgeLgHQsTTe42A2\"\n",
        "    \"CgMwf8I/u95b0RTMZNpZgt/t/C6aFKX9rpykFZZMEOoUDB5h9TvgArbf7KEX5patR3SaQZB2fuzgGtHroE6de/P0EexIZJw0U3DheqtkIvTFykra\"\n",
        "    \"3MFnNEF2THH6BKV7HVvX6zJcO3u26hYLt+JWX2D0WnfzWkEUD4ktl2pNUK1b6Oq2GBCqgrIrE6Q69FIlbxYH+qkd/ZZeQJ4jE5/kqC8jjnYmIsTG\"\n",
        "    \"wXyOG1cFrNB/x42jQO5TBHm+mJGvYLZWghAYRJYkroBvgIvIoi3NCMu43BNWyZ5lHSVpoiDgAEgTBSG9s2AYI6ctLSBaWgnCF9wmQRhUnT/NnfYO\"\n",
        "    \"wRKg3brB+bIkCB2v13nZcNOOi7v5R8dXxxdHxxc37PDk+Pz0+ubzHy+rSoJ4fhf4Ie7cFT4Gepz5yT6jPaEWRqfkM405PenhPqI24OuJrVjhHqNv\"\n",
        "    \"jXutGDE6OWA/Q6uT1zAIB209zCrIOT8BR25+Ij05/fVjgJ8/BuX4N3XtqFER8SZYLvGN5i8FomC6YjytDYxzXVdNEkFhwhdFBusqxa4qNOLexj+J\"\n",
        "    \"+PXUXoj2K4QiOPYL0Iik/gKUnryu7hXWsbjSr8b1IYjiACNNZCisLP3Q7MTGkXZsB/aErX8eajpKGSK6O3Z4edluMfyf4sKS2IsTE/gwG4pgTNlm\"\n",
        "    \"lbR7ttbQefXxYCAClUNpVfvAs0VuDKznADDNhSVdMbq21oyybI1BzmFlGyhtKUxaCPNF5+6uSzkf55dHx2cvvPLP8whcfH/Cdxyz9MJKZkkGsOet\"\n",
        "    \"1zHbyOuisXYUlj3k1lKus4lkUEqu179zfIF1HObJ7W2p+9tbNguWYjsOF3noK81ogtBGq8O9qYe76+0wLcpLogGTux4Te/UyPwkEHiNnc44LB36h\"\n",
        "    \"/e/yCv8YW/smFY40S+9Na+ME43ruGm0kfderiI/5I25MWKk7IJmTCKYLpnDBsouaDybZI2Y3MbGn3M6DCZdWCAyWcPqpgrFeAeefwb54A4vPmij3\"\n",
        "    \"QEa3h2a1XgVjgXqHwZgORaVLxfWNVEPwowGFd6vNNjc1/e7udDaQIKLsw3qDYzN408XeXg1u6mdt16V4/VAu9bg+ANA2ou2uB7pbyw78qWlU+WS9\"\n",
        "    \"ykctT7TtEi8xXJjP+Rijr84H7lyeuInLeHKHW/q2qFEKlFLE8xNPhSTfeB3kC8xN+Pedt2cCQvMTX0xXjNx7HRGSsGwZB4QcqH/rgYcCdi/+2toC\"\n",
        "    \"d6UmtlQB1bDEGf/dMhXUbMl4zjHjSkz+dpRsy6d0UZgosre6pgM8F+mC3yIhAHf/RcQWPsl1EUCNMfDP/Wgi8gv8xXmL8adxJNKayAmSK7rlHZlV\"\n",
        "    \"Xi/4NSM7D4o7qSbGM6NEKZgVJeN4EXJfAd28/QE/Wy1WTSeA2qiEm0YlU0ZgUfBkIW3ubudPGKADn6MnAL9m7jbGwqi+TiyU6YQrqYTskrLcZLc5\"\n",
        "    \"xtoxko8bbqYP4SktEmqCJrmG4ooMQOjhlZDNIlvMlPqmpDlp79B+2jzIeDJeVqL0NZkUKkUAxuq8khxCg9eUT6UEERCtLm+/Katme3hQS9vvq+kl\"\n",
        "    \"qllJODD5wH7fvBhYElTNWnjeGiLnqp098jcpBNa2t/YkUlhL0LgdZWkQjoO88GnZy8vKUbDSoyIy7uipTKe7ov9lJUMDzbaqWIsJ56xrXmK5reRc\"\n",
        "    \"rRpQdPsl+6QyAGCUU90DXRMftpSEbJWnod6CWZmAunOCMKt2ag+d6lJ2O1PdztZ3u6oLZcOOUoNy2NDfgAWlPGlBr/a8F3YE91z2y+nF0enFr+y/\"\n",
        "    \"2NEfFwfnp4fs02/HF4cn8O1lzcv7ECakj0lJ0gHBcIVPX7UqfeQYdMj3KWl0QBmNVLJmoUdTuxJoEmb3M4ISYJc/J/DwMQTFdu7KfGCJIKg4mTHd\"\n",
        "    \"RlJgYcKcSec8Gt8FPI7ydhzd64CUq50bUa2vqCQfhxwM9Hpem8+SCCpSHo1VDJhTEXoWltAYdoKkOGKR3vPQc6V+lRt4nz+IQQBr2ZlzUAdFRhJa\"\n",
        "    \"v8Zt5GN9sO9ZcaG/FBG0E80pRR/zqIHG7ZkaGdzAh6VnjDka0Xhb7QGZSLcehGkwN/aHirQ4GOFrs5Im28YUDjuEA5zD7aKdjk3Na6wlMvfbFmxc\"\n",
        "    \"bGzmlpvsliN8G+uC/W0iTpsqyhCk2pMrVy05qEDIi2qRN67OIXw5laEgmqQcWuHY16acCPSMM2if7aA1pafNvrCuaJ7sY7DvW1n+mjZjqMZbrG7m\"\n",
        "    \"D3wTyTuVdX0f/SoJrHm46K3BposbmhY2tMEpsQHL+LvYvFnBRuYEbULnZLoOHQBYQWdHo9N7Bjp7e1V0dr6PztVoHTo9DKyWxmrPGqvd76Kzu4JO\"\n",
        "    \"7/voXETrB2uvwp0frcF6933urAzW7nPQAT21drh6FYS6G/hTh9K7Og5twullE8veusaC+P8IU00yzpVPAOQ5RVoEMVn9RG5LZAQYq0LYV1bICtrr\"\n",
        "    \"3OnyKaJggjsA3f2uPhAV8nnMrZyw6zTGHBzqEw9yIazXosctfNl2Poav8UGcSMEnj/2D0hIkVn2xfDjybIfLCjw2l6nYFAFeJHgycMpDj93eqoa3\"\n",
        "    \"t+rYljidB36C2IeyLZEyPXp7Cw2WJbWlLJ8ooefx4oGDE1d2twgD5KVkpFgljfdAK7PlPSgHxQxDUz2W/KfWSgs5XE0hpVXvZ7W+YgQ0UY81Pbgy\"\n",
        "    \"anBNGSHjFJgVjSOeiMyw4CHFfNMHnoFsPIqItGAW2Gr/onMvLOFTSo1qjySoUYbJpaZ5vhgVFLnGg4Y8yMCFxqMxMZ6lGeGBH0wjTh/LgyFTAkWi\"\n",
        "    \"CvF0Fjwh5p7YlnQGVc7LqDJYZPfwBdoI4dgWMLCieRZDJV+FWGADsEly+P9erP5pKhOF7pZzMP5GlGQlgjj5l6xw7l3rpZDZJXpTglqG0UMUcke0\"\n",
        "    \"A7QIZpsBqHRR9KHCv3iW5j7avw6W4eFRDMX1R+xn1lEQObdjl1TUMh1tKRl0xG7bSOczEUQyaIHKemBSZvsIUYKh6Vi3v4YFMg+KqioT+dRHAfFP\"\n",
        "    \"OyagIGUVjeeuDyalPzuvKuIXCBhV/VGMSJWDQvXKrQpnQ5yotSEiZenIeAEuQw7SO+ZifoijqyIPcP3J2Ne0N6FPFkulWT31Kg6jDj4Naw/L4qFX\"\n",
        "    \"JrNYRPQbJ+UkyvBMAMjIPS+q51e3yyedS9v6OMXFPMKwP8esRdXcnGRV+JccDTyg6hFBdE745vLm4IwgqcXDQSAU6xphryr0KrVrjCs7cdv9SWG2\"\n",
        "    \"/cU+e6zR+vD5+FhDFYuQynU1KxBqcHs1EMen5TJi40UhISARhvsnlpbDd+ksKqhLE6xDAm2GCT11pLuNcn1ml9aYNrENNV6Eh0ejxUzE+AKViUxT\"\n",
        "    \"co5CO5ExXHkgWBypoOaCTYE4GrxhIXp2cE94aOT99p+5wiinTs9ou6U90QGA9brardRBtYbJilXSVw8mk3GD8Dp9PMK2LdxLTFYPXLkBNPGl0KuA\"\n",
        "    \"PGGxLbpToI/q5Hp1sAlzol8C6ZLBKPtRk0N1VGLZlmqsukRxEqeAafyUVK9MCkUFqDiAXA7CS85VgnyV8Nv6XZZK1L1fDvJuaFbVv/06hVzSrf1S\"\n",
        "    \"ULq0A0GUwcjpgdoyvFQrTQ5iRFSrGPfqpvxaRV5PR3Wn1uzkA9hrnkWYEaofzd69PMgst1EZmLEtVMkuExXJmg3MmcMA5/sUplKC+0c6VF9/WlDM\"\n",
        "    \"kgCn14ZYfmXTHGqvZizguPdV5PUeE3JWoorBwPZ9hsDAgfay6MhbpR87tlOKjUG7kmslE/5KiYtrhakuN3FtJmE5OBO2XjxA885ln4+vry4vro/Z\"\n",
        "    \"4W+ffz++Zv84vTlhv10cHn++OTi9uHnhzB91HYNPVyXkPm75++DV8AyP2RXLZ8j5pnlaJ+8tcfz2GZHf6k/ii/NJuTiapM8RgRJ8JoS/fDIJw9t6\"\n",
        "    \"6v2iM+3aOrxMJtOMBwmmXGY8CIWrwmfzKCOLBSYq+haYMDCCklyaWr+JjHnwSARNqDQLclTI8cNxwE3TjFOCLiZPtlAvJzK6jM9RJsVRDGGOdodJ\"\n",
        "    \"BvRAIRSGZXTLgDyDw4I4LvcndPyVRlVsH4psC7Cw4VkTbCg7PPogTBuiC6+yGMNqJYwlANouUtJQd5itlyLno2IpLBBKCQ3JZsUDPRloo1xfOcOB\"\n",
        "    \"lhmmIyFXBWIHuHbl4JpzHrZ3e3ScKCIcyV2WptVPsJrkaAOZ8z8IUxhWYRY85mW7RZ9Yrz3VrraeNAftw0RmY6jmaJJqgNEg9fwfHFUyd2Y8QytL\"\n",
        "    \"ZQR1alaq0pw1G2JR+CQcsgnUT9IEPcLvHtjdyBYSfVjAHQCN1c2E1fwSnU698V0agS8Nry3KCu5bbJNCL2/CEP6FWBhk0tcaBAcAbViprp621ZOX\"\n",
        "    \"L2aSpULnQSXjR2o8SQgG4mqO/SEah5WOWkTxkNi+pkgDgxWqHNCrW6LWwbcNqDUGjR1QwVkjL/oAmoIp2ONEJgjBU5TjyXHJiL78LZrlRShaUUzB\"\n",
        "    \"ai2bg1WLkEW2yHpYMrsvC7nctQmyKbhdRQUJqxoPfT0OUL9AqQ/wAJuPVXU7qltuPl6QpOMxNrFBtJjh2KrtN2oxXNdk0O7iwMrUFiyjfHXSfWoj\"\n",
        "    \"A38mzS9fjQrf7/TCb3gQpYT5oLpfDkTPgifHQvB9314IttVGgcStJRphJNHB6SM0kNm2Hhp0QFNYgMBYd/ZarAf/v4X/1cm3byUD1zoYJC7j2BfS\"\n",
        "    \"bZ+9ITqwQLDblJi4t3yyT/aI08LoDo2pNYqtKUZ5gY+0OFqNihAPARWh+ba1pXnfUvi/oFH1o9r1ouMYH05//Q1sLHb5gZ0ffz69eVl7CtzSwser\"\n",
        "    \"q5yn8l7rcmX7VBj70iSyf0lbP49TTPGgBXLM5zIiOE/jJfThgH5Y6g1PvDAKVT+2AO8FMxF0MwEr9zM1w3COOEtMkIZWdvYXVCpU4FFXWno4fo5r\"\n",
        "    \"V8x60sVsK8DbqjGsDPLpvUrgUv5pKeGLUHUlwY5GVn/J9IVP/ojngqVBBuYpWGyPG3lLTj+sygAz1/dDrTcPs55f3KFxkcahHar7EeaSGSW6XWf9\"\n",
        "    \"UIFFcYZHbnMKJ7fxaK7AlDngUmZkZgUFHbJ+gjWUPd7htS/yhjMYTXbPMc0NQX3+s4fKQuNksmEdAOsTCB8Wk3iRg15prUgJrJ46Z+wUr+JQmIBt\"\n",
        "    \"Or7jIp6moQMZYAuC5RiM79V57DuY5kBJGzERbUUAAI/atPC2LIrI0Y1ZYEg+BsscrBi8zUxd8UQoaVdT9+XfRYU0rQVAAEH99vHcsxqyFtuy5tBg\"\n",
        "    \"35SAKl+W3lXcG1Qip3sRxX1sJWCoTZ/wbKh9q0kdz0RIyfQLEEWH+FC6SwXqvtfID3aG5dtSbKo4MaxugKrw+mU5LEGsMvB7YOU8KzeDfiofquam\"\n",
        "    \"QlxOO3mho59OfLqa768EPKqxjbU5QRU3sOx/tdb6e721/t6G2bymxV/xCNeA2EILFhZ8fxQHyb3q9ju+5KG4v1AykfypSfU2RJnKtO5iQznLb8R0\"\n",
        "    \"1GV4oSBAgwE2MexPqLBBXf+M7up7FCHLx3LJixO7pov5HF1SQ47Kutd3GWJKT4hn70L+EAUqK1QcW8dTAYyaiQWFMAHN9YkyUTG1n5JUMaG+RR6m\"\n",
        "    \"5SYK7xnVJIUf00dO102RV6LdNe2lkf5NM3GqDJNFW0zkxfKneZzS2OFdXZi5T2fQkETgCD0LB5XOvwCz5Q5raTt2mkWhuGtVzBIACgOCTjgwVrCF\"\n",
        "    \"YJf0LOpxpWRtOVRXWfqknTDuDVC1t3R2ebR9dvkJD8ETrIvgQjrVSL/WJgL4DG9+tA7sgMpDnaUBoyoMo2CagE8qnd1jjPzJ4/ZzuupToN4S2yxk\"\n",
        "    \"Maxcf2l5w3Q7yOK87Dmry9sUJUZhKZ8azXF4xDsy4H9nMAArYEhm7pSns3wO7h2e2QKbVV7cs9NzhyZZ+PtuuTGT8QiDvjtu0Blikm2HbsoTV1DR\"\n",
        "    \"Zuhkomt/51rB5qE9z8wlc7SQo7ShQNLQrdxFBUtkhooQ5nmMV7CMQRzoerDqpYfygix9saItLvqSvw4QZhfQJVubEC9VVjdzoYeAmXFD07s9u2tj\"\n",
        "    \"GojhnPLwHauuS4z9zt1gTRt4zY2C4jyAdTmZXb8vjT+TR6DLKJVAv63sEOmIPgrOM2KaKxGRVtUlesaPia1ZsQu9nvT1kzxX6kvPXBjOgORAeEjD\"\n",
        "    \"MtQfLLUtFKhsLjx0aoc+1HAVIdketwbK+qwUghOsIiXgy2uf+3hLg9AOgsHYWXONvJRlpTx+6PWKMSR8B90hWPSonq27SCodg6Yvz3S/hK0409yU\"\n",
        "    \"GebrLB9UFzV+QlktteQY2B5Cf7dVmmR9+8WKqghF2S95LwInVMAzDgawNfn6lArgrFp2pDcELB1M0xocbTsBX6ky7Q6JD2jltmEqS8M3TrGnHTre\"\n",
        "    \"Yw/DtlLrk1rkyNrD+R0kEswXiqGLDOT/DM6acIOcZODvrwSrqHyOt9LZ8S0VUsCIIBShkieB1/tCbk1dzjGisNtbiaca47ZsvTWJLtoYpaw7KVHW\"\n",
        "    \"FLKum2ZO+Anv7xjgGFieQFMLYHN/1Y011bKeLgcLvVRgmGlVsVYCq3LNAGCb1a+VNso0kJXFbdrlKmIiQAWcXRaPLHW7b8uFfQWLNZfxHhbr1aoF\"\n",
        "    \"cioxIBLh1S2VfimXfimVlk0nm4yNclkZcEQBlwGY9zbEpsjZcerAUCg7pVVXd0n6rIz6fwT1yyaoNNcFrdUwnVgZfBlYk9pspVhE2EgB/3+E07od\"\n",
        "    \"l10fnx0f3pz+fnrzB8wNvOnk/PS/6djlC99HSEcFaB76dDhi02F46WwymXViZUyhibn+wpxrOu2BljudThgt1V9y0H2zJ/v6nCdRrswM66S/2ojr\"\n",
        "    \"s6/fdGzCSqxYvTUYd3Wq1/qaaO/zdnXKR5xOS/sZVl4cgWo9by9iU9pGzR6E6V3zQBOjLZ5TuV5yjBjOLTsN9DuGDQTZtFvngx1Rn3pp2twPibv3\"\n",
        "    \"FPfRnAedcK9P2UltbQ9kX/ePqTpgmLDXpktZXR7+sZttMX1d21s85FHZg8LcOAlXG6KLuKDdKvCDcEtHiwOYsg8lnD0wdWe5Y/sk1HowaX69/+aL\"\n",
        "    \"SU1XIT1YsMEINPhRqfVerof0iBpjdZGkXKpFlZfVDl2XHZ0e/HpxeX1zevjCVx5J3xbcVOA1/jGCWnXgF2AJribXlMIvVxlGlOjaXXW2R0C0b8IS\"\n",
        "    \"gQH5dyZmHB27nOFfZnheHo1sIc7ICZ0go/XQt9PsN0Fq3nZc+5vhHPt8fHX5+QbmDeqkK3MNMiWhIQuUP7ke3KT5P4nVEo2qffYVjSpAcb/1rVmu\"\n",
        "    \"fKRu1qFVCWqCmL+yrs16NfTAcAZB9XqTb4BYXTnY/7JcX0GtwOvbomzwltYx4LsGfKVcgO/WgTfJGcz59Icr4ZtZasDvGPDlYgEdipuW6ps0mUOe\"\n",
        "    \"W00D2hmhFm6zNOaD5pclahSadsKwWdm3pu4qrTS5gGqlbXmvmEhZ23il40pj0XNFUtrtNl02pC+GucZg/oEINtHf8sCbdat0jtPUH4GXdkI9Wsse\"\n",
        "    \"TkL4jKpusIvL8lt1uerQDhc9O5lMwjNLTR0KA1nLUA+FagdLt1Q0A3ZfZYP997vf9hndgMO+mjb73i4IC2P4VaJZKcHbcb4C4vZndX/IfehjFqK/\"\n",
        "    \"bv3W0fK+VFoV3kL7GYcJllCCnCaJwHqiRJFVHkayTz5SbBRplNC/WeOn6n8M9XSUYEtTUH4z025xjrMBe9aFEg8qd8s6if6qlvobN7KfbhvMMYJT\"\n",
        "    \"jazSHwnBULfb1Jfvcn/nzd5K2ikmfImrU/bwqcQ9bLPbWdNmt1PbRnNcdmgxW36pqQewKvXgS91oYKKtI4nGkWmVBsWtGRWkGY8hfJW9C52Ft5JY\"\n",
        "    \"3+FFfReJ/6r2ti7tGVmcpHh+vWZX5/tiCE0FoemsTtbMrsW5+BtOz6Px2naxRUAFCIM+Bq9s9/zVEMjg3zCohfwTG6SiWtbDQjHfxMgjioNaX9nS\"\n",
        "    \"OQqBM+sPTKmeK+7mq+EAtJW3M6XJsKZGV9aAObqqYo6t7YizyyPdD7nEr3RLaHF2+cmUfimV1sDFLQPheqrtoTId9Z7pq6HiVeWmFzFdL1K1Jy33\"\n",
        "    \"jImLZAZlHOxQuuoI+P+T3rVYJFpbe5XxvaDNFxEMUGjZAQKDS9WAsS1UKYTqqsNFFIe2qVW9pL78R6HW3li/3hc8TJMHnkSYmrivMjMD+69QoHkY\"\n",
        "    \"iBtW5S2ttWfXKRZq/aUqdb0pLgXWHfs2hn3zWAojl/+40LrbYTFdAiaA7+Mf8/F9+nMgvo8bRr4vrxuRfJabmxhfrtym76kxJDxXmE08lhVqDHGt\"\n",
        "    \"U/tvvRUz9BwtKHN9Tp1tx/UqsmrcUfNPf9QaddRuC/Qr6bu/Q8v/A1BLAwQUAAAACAAAACEAcgEUZ5cfAAAhaAAAEAAAAGNxZF9zaW11bGF0b3Iu\"\n",
        "    \"cHmtPWtz20aS3/UrZrF1tYBMQqJsObESpjab2GvXxnHOzu5VSsdDQcRQREQCMABKYnT679ePeQIgRW+dKrHIwUxPT0+/pwcKguD1502+yq/qfLMW\"\n",
        "    \"y21VtkvZ5I1o8vVmlbZlLRbw/w//+aNoZNHkxfU3oihbkRZC3leyzteyaNOVqGqZ5XPoHh8dfVqm8A26pKttK0VVlqtmJOblupJt3ua3UlzlRQag\"\n",
        "    \"RqKs2ryEfiLPJPz7Robv3r2LAI+sLldbxOPkKF2XdbUsNw0330NXIS3SI7HO79tNLQnYHGaCzm1ZnNQyzQDXvJEjwCUDFPOyPmrmskjhk2jupKya\"\n",
        "    \"WHySUmTlvDkxK47XGS0aJkkRvYaGp02zWRO6TXwUBMHRoi7XIkkWG5w8SUS+rsoaKQP04XFH3CdL23S+gvGyMZ0apNbIPhqJWlardC6PVI/fm7Lg\"\n",
        "    \"4VXaLmGteugv8FV3ghEtYLrmju22AqLqfu/TCr8e6b4FYL+FiUVRmeGwLmiA/6pMtzXzvNoyPPoYI1XX+R9Sw72qYcc/j8RKpk2bNECjWjZmloV8\"\n",
        "    \"nsw/ZwkyC5AQQK/kdTrfHh396/XHT+8+/CymIpjEp/FpcPT2tx8/fvjpt0/vPiU/ffh78g94VFRxWtfpNryEHiMxPosnr+DXefzyK/g1OYvPX2Lr\"\n",
        "    \"JH45i47evE4+/fL6h3evP8HIMHgjnz8LRgJ+f3h7pj6FH95G7ufnYfo5sl9fjIPo6Ne3rz++//Djbz9///7dD8mnD//8+MNrxHLZtlVzcXJSba6a\"\n",
        "    \"eNNcN/F1eXtyV+cnk1evzk9enJ19BTyGq46rbBEcHR1lciESoPOVrMPbdLUBxivSNfx7DOQq76a0pmV+vZzCOvNiAaxaNiQR0zfpqpHRxZGAn3yB\"\n",
        "    \"dAAJWTOUSPxpKk4FkBMlD4c2i7zIW6kfwxP6JL7FaezX72gyBoo/dQrCIP6Fz17XdVmHi+ABEXwU600DOytFKhgy7H26SmuRF+LyAWA+jsQDwnqc\"\n",
        "    \"AcEUjhp3kg41P+D5JdNpEApoLUGUCrFYlWmr1qapmhetvO6SdQ24rjfr6cQSLgcV1bRpMZe6a3gF+meEZMMPSRRpQg50hVmop5ot8kirZvsichZC\"\n",
        "    \"gRLfTcWDgvAY4LL+amQ/BGn7QxbTX2tcMDWJH0rYf1IiPF/19oLpApz5VXxKbfJ+nrOmSYq1ffz85bnqQJpDFtftMpk7HSbqMdBeNsnmff/JQiZN\"\n",
        "    \"Jec5Ab8QSDh4SjzKgGt4WKnJO8+pA+1ZAtvbJshPSRI2crWILO20mGBzXL0FiazeBko4Ji+i4X7egmGI9z1wpIkpOTyXSxOc1v0e7BikKQX99Uen\"\n",
        "    \"K7Adg3apQlKBbEaPfIKakYNcFPhwarR1oGL9TaElHsBHv6Q1cGQra8VHN6CaQbRlf89vmttdj3KcevjRCp6BfZCnCXCqeaoZsME1zJPPG1nMl2CM\"\n",
        "    \"DKsgiswoW5CY/V3yopB1sshXsIr+U0BtVV4nWd405WpDvHBjEXkRf/1qciBLoslH8UWlFwaaUGgs3EUGkb99mk+uJay1ZX5h/RRpNbWDLTvzafLj\"\n",
        "    \"hIbgXzzbDgYeohLMM9QcsK0an2lbdXb6NJt9fwXWnpyjT8Ciba3VJPhJrxcLaEE7wdPcymysXUNwwPIV8Hj4/n/GEzFfw7+wjru8XaJ3ucpBvEHX\"\n",
        "    \"lLfgsoGajtHrQqh36a1UMouKr91UK3lJWz4ScRzPWDsy7F2Pm3JTz4Ghm7Y+kD/uVspBadhFIcJ6qIA/Bz6YnNJcdicAk4GRCr8dY0CnhHcrcgLQ\"\n",
        "    \"+k/QEKG72izTSmILPOTP8AA/o4f2rTiDrx7D4I/yGtLVKnSdh7uVtYUDj2E2et4HB32LLQwnc88guAl+ZfligZB7z5AI30JT9JT20wxkFV9ezMGX\"\n",
        "    \"x9jD+hyW7uydK7elKEFZXKfURRHYV9Qdw8/anVhhhKxgCOI8iKE9r8LDEedh7ARkspnXObgCeds4rIzTqGALFCREPja6QKWu+RHcIJZwj8328OQh\"\n",
        "    \"7Iis9RRL2C0WfTa/PJ09wRjfDY0aT2ZP7/5H2HTZtBA+uju8AERFuWkbjP2AZiI1+kY0iuzOPis/UjlydQUojQYwUm2KT7SrmdegBa2hDY3nhXpx\"\n",
        "    \"v6GJxPg7gYGdUX7vS+DIzRxipXoouIWhi/weFoseEIfAKepK4HEJIeHZufghZm74uYRVZeX9SFxtQKHWJ6v8Gvkeo+qVvCdUIZyG1nINxn6Eu6hA\"\n",
        "    \"baFTXaMOLouYgP0KFIRF5JnAQB+XukAmXEF4aGJtFS+NSBrkray3RBk1rlqmjYz1Kum3tjc7nDn9eMgMHWaDEAr2vJPQ0qJK7QWQzziATItrGZ5H\"\n",
        "    \"4hjoygbDDJlADCaOj0XoQhq7cON1eh9GPN2iTuecAZgaGCf6UwwSG3K/9POmTOZp1Z3AWxRM85xQAjRfRkL8WazL1clPyDOb92788xCYeYML4qfw\"\n",
        "    \"D1BANtwdWcTiFvajaQHhaOSJVgAqk8xuovsGiltDMxgFuTNqUUuZ6OWgm3thFtfpuSmuyk2RJcaq90cArdy5HpWEOZmgxEnkhG3ZpivyssH5wl/o\"\n",
        "    \"ayOcdE4CgW09peOpk+6Spz+XBYffAKUCFPMik/eqldGlL5ER2A8FiAbnr5AUlLwSFcRvymXBPBb6E5loS7FEx7q8loVEiUFshcrzKKmldVyIULWO\"\n",
        "    \"NJQm+mZgcU7HiGXrQwEyCfjZPJkTG4A51H4UivGmwlSEzGLxDrgUqQ+tILBEVYKmdsz6YbAWsEVFW5P6+Ean5WBwoY2u5PEo9psr2Kl260s9P/Vs\"\n",
        "    \"kN3Gnvm5yfyuap97/SjI6vhMfV7ojwPTRrP7fhNqMG4mLwlabjLT40x9J2fqcjLDNtt5T7Af8NJRCf8jEzS8MYF/qDd6FFGPAQ6wiRRarfXsQoPN\"\n",
        "    \"6WxE5lh1QOSn+zMsATOhIRMjtE7b+RJ9IwkuxhbRVegoHNIV6HMmN/RpklV+I3kbI8SvJ1bIWSg2QkLA725Sr+fwHtF8dsGK3NqbVW4EI9V1I7n1\"\n",
        "    \"OzGJ9lFB4+EocIcQegtoZ644/gJDM3FyWyG5s8NOUgo+EsZuKQ0k5FFjsbrCHUYU+66zWgBzzbf+qpjfO40gLLT6vSv9wZXg5qS781e+S6yYVXvR\"\n",
        "    \"iOtKuc527a6qxJ1GMuBuO8kbnY1zu4Idd7+CCT/1fE4P7HdTAW6Y4rGnfMIuQp4jqDQg8d9A+oaVPOOGxqkXjRtfG3MVMkSl7yCULog6OWkjxXzH\"\n",
        "    \"ZBmiS7YiFzOwcjeZGYEePcqSM/JEhBNwCp45beQ7pPd5M50A70hZgTZqGCO7BtbWU6UN/8qgu04uNY64r7MY1O8h+ZaezeMWxB+T0W7yA42dFsV5\"\n",
        "    \"WW1Di4eSJdIPi1XaAkf9AXZPsfJ3nX12Jh3mHj3dpdNzBtAtbl5XMzt/uFTf/zR1Z5p1YzzuZRS+FmUta12eU4lnd+42ra9lq2mipp0dmedIZom5\"\n",
        "    \"YWQbdPaszvs9nZdXeVp4WX1v6QoaQudpjhE9eV95kCJvoLvTyBWWX71uigAaB39yZ7Ghh8gzhmu+jxViEXAvf/LgQOAEAYVEPAity4uR0BRy+y1l\"\n",
        "    \"0wASzDtZnl7vnRXiJwM5/hU4njcLgSMLzYBK+nE0tH96No2yGXjksofLGmBOJz6FtL8OKPNJV7hK11dZyscAF3bLL6lhFqGVFuOXr+hwB//Z66IK\"\n",
        "    \"cQ8e+3Qix5OzkajNZ389dhKNzsxJJwFP+TiTUiE3bAxkBg6aVGGfdKS9fK0VWT10Gpld8Nk1RxnwDvtCjR6dlcxXEJsoDJAQX+OBHa/pKVII5NLp\"\n",
        "    \"DvqqMxnSiYrrm2noEPoA8H8Wf8NxY8qlZuK6TrMc9lQAZchHhnC2LSt0qsEz2NQSvO27pSwOgHsn0xvjL1dgkDCEJOY+UY4ThczzVUkutECFGR8A\"\n",
        "    \"F+w5DLxm49a0shrhp0LMl3J+A2H3tqF0EexCnm1Au4CFh0j5acgL5DTee8OAENxf22YIe5NiIW+n56enO7gReCG+d9MsjASaxo5YO8oJfOpDtAof\"\n",
        "    \"emLsjXr6qgk1dPCkwN2T46+H3ISPmwIT88YZsiUGTmAJAQ8fxcwNcUtYbgPES1eYjeunjhBzbQZVJoDsI9vVJ10lTkkiSdweM1gIuyE9u8twfXeK\"\n",
        "    \"u/JcT+h+GxVKFWEhv3mO/OHenQusg/8UuSQkAWSr6D8eD615rHS818roSNw2bWHVaN0ffruIaPRtphl5hABo/mAbCgjiGav2y1kR9Vxon2/cEhhg\"\n",
        "    \"u82qFbd5CZ4OutF9PnHSNFJyxgM/jZycC6dBHHcRHnK6hJ7Qx04+xV0t93JbYHw5n28qQGMLjxQ/BIhdQlTgIfRR51lUQYsMswUAy+If0zZ9U9OR\"\n",
        "    \"FFGHcw6qTuSyaesRJ4hmHcU6t2fRzrk07Jv9EoKKruxRo3PsCN3sl7Crszl5m3aw6J8mzcT/smBN6Vc/0foL1x9BUKaqgf7SeCqAQr0a83YQ4DZj\"\n",
        "    \"TuGBsFDNECvUCtYhTbp1zCeSkmqfKFNLGhnP66q36PzlgM2b01h8wG3JAZQ0B10qKYsRZq2OOdP6hkyLOhIV5rwTZVAW6dUKMzc/8mkoMCmFp1kG\"\n",
        "    \"zgrIcWNCVlb0/0WAMBFLM+jiK+QSCgQpPk9/JjbJa6zlaIH6ajIU+HWZSZyQoH3vHMyVmHCaA3dC/wXwMmp3GMMcD2tsJO5yi4YSQ2D06AqIpxo/\"\n",
        "    \"KcQlP3ECdiPPoHdSlRVyIqbTs4VRRUGzwUlkQq5KwGczhQBu3RPw/mJAiULKDBNfDhDCyqaxKBje6oRtYJNLCa0RmTNbXHbwmMVtmVC1VDiUvNiR\"\n",
        "    \"FXCgRt3QXs+mA3ynjZPITxXO+Bia0J6nHrkhPpe6LVMM9u1pWWOOR9QCrPzvmfTTpqrwTKBl90+wYVNp0LyYrzZE5pScG6XEOTFyBeryRs14I7dI\"\n",
        "    \"Z8pSm2lVeh1Tb3KLm469LC6AJbYrhlDs9P3P3//026+vPz2RLlgE/yxuivKu0LheiAcA9qjQcROWXGmmUwUGuUvoPsOwfBt1UVSGS6ktZB/Mzuuv\"\n",
        "    \"0Pfh0ab1pHnCgU0bdlayN7PzSQEF7dumOa5GbPyFqRXNS9A8+RwdWyT0pZpEa/9E9QbJozWNHH0eV2/7S/QytXOIP9ZFAlprfhNeVmleQ7xDQ/Az\"\n",
        "    \"jnGnn9GZi1H3sS6iYJDN7U6Yky+AqQslePE24TV1F6bLdDBkdCTwpC97QxnQEJNTSAyVjfGcJjZBut39ak8N3VZ0uixiXhUPCSsWLgYDMqAmw9aY\"\n",
        "    \"vlCJo+OretN1jy29TXaN82AtiJPsoSjR+HsW4uXAiZKXdnFn9OrDPHnt+biIO39x59p5uqSKOxzTPt1/oMS5WcsnKkH7dPA4jPN0yEnHRsdRB5Qc\"\n",
        "    \"NC4dz42x/7N4rUIqciooKz+mzH1Vl1cpn7OAasFD29X2GzEZI6cS6Aij1rlckUfSYNCa2nNdL1vpYeA6prOBzKXySvDABLOVu3KVEZ0HOLzULfAy\"\n",
        "    \"JwJGisDeM2VUpRcGi2YSVAl/3YMp9JucnmKs390tH41eFdkwHgzFLSxg+y/XwGWqitCx/ko1oDN6RUUbNrLyl0bDi+vEmoVL3mjlXSZcEYLTPTwy\"\n",
        "    \"McwRXGejdEiBQbT3wEQPM2M6fx9pvS0BYYl+GSutniHNC+Np+5F9BdHDvRJzeEqWL05bV3t4RZZRf/h6YLhDYn9EhyA0oDuenznpLrWI6ZRV5Y4l\"\n",
        "    \"KLOnqjmShXy+cxVdEzi8qAGAzrr2wtixzIBBiqXc1OAOgTDYmOGNW9TCVRekD2UzhtiEeCXYkwJkMows9jpLdhBem0JFBN9wIRLwJbJ54A3Wh6uX\"\n",
        "    \"v2Mi47SfWO6IQYyKssiQJR3zYqXpGWgCtXvPFN5o6o1o4DzHGMC/hF8usd2KXWVhJVVeUN3F2NELpo4S05925hNx1lVkbpHpHiVWQaesbFUKBjbw\"\n",
        "    \"2Oi1Y0SDFYypNkHlYqPOHZEFclK1dYpDTaXJdKDUhAO8qSrl0F3/qjHT/ildftG9YCnN59rtHeqFjBlghKQ7U4vU5a5mvBl4bKYhGJZ8boWspQmm\"\n",
        "    \"+RR4PEYmzIFzDH2LlFOtFCeidrWnNJ7u66ZGZpzxwQ3fnfJhzYzZm6mX/AgfAu0/X5BzNRKBLlnAzAoB2e0cHB9Tjf+Fp52xZdYp6NWavJMWchNB\"\n",
        "    \"X+KD7Pjp0aaXFIoeHztpK9oIWKrKFSwU/Z1dASDW8cKcQOKN6WauTEgOPn0GnZj9nMLphCqZDF8NTunD5LtcnEGAofRbL01n4y5oe7HajJnSIKm+\"\n",
        "    \"d0EO5M+sy0lo6y8jRyUmrDiAy3FCX8UNIW3VK/TvtOA5tFFkmCGka2COneqWY1kRs71tW4QrgJ3f4NOg2RZcTjqCQBEmBd5Epghs2ZVTKoBqsaCQ\"\n",
        "    \"0GvmuprjkcrOT8ngjsQVRGrXNbItHxbs4lu7kOmu3ODUywZqy+9WYfkIWUOc+A/CHuLM5hZXHZ47LX6Ers5eQVxtl30hOdVNAt0wE6bGzum6H4QU\"\n",
        "    \"TSnQ3vGdKQtOp4zLu8a6hagoPOw55nWX46DBCempm84diYfjYzuJ3i5d4OeBih5dV8XdCkN95/ijvEMnNfB5hRSj19TjAFCLoEsvFLbsWDiJhVAp\"\n",
        "    \"nV2KZdTTIl+qGz1dM+ooENCB9sQFsEpGOqWPyCmcfeUyiyEeq3HfupXf3rIO0fKHKOmBY/ry7nIRPCh8Lv+ijNZfZo8J57PQc9NPkd7uJjba+YLP\"\n",
        "    \"nvb3LCF2jJyizLSWib4CDJ7Pbu3wxXK+azO7imVIH1iMiDWV4/XMOF7PxLs3r4FBQ/Sg+MDYHBvb3l4fqsvQnQZwC3qwnREWODwTyMROBxfyozo5\"\n",
        "    \"BDJ0hJ/vBYWM3EivZIQeZIR8ZZeMXLj2WJBIjPDUheHQE+hOPD7tTOE8sZM6ru8UMXAOJ1PyAg+3HA5jDOucKaN/kHQ7zOHqOs0iPeVF6F4G9hI7\"\n",
        "    \"iQjS2u9ihIO+dcUDV8VF0ms02Pl1UYJYcO5HlYwdeufuIxai886tyjulnZmn8V6u8x0Tcpsix7vc/hU3zhZ80W1KmAq1Ec6N07olb+q5mWvHbTHE\"\n",
        "    \"zoDAL4fB0PcgcYS+OILr3p+vd6YxBxtYHgiYa90E7oysMQnebvdyH93wbzqHif5uzEh/FUlWp3fN9MXXOxixkTKbvjj7NzTdQSotCIJ3wFDIhFiO\"\n",
        "    \"smlkPZ4vy0YWvAah31LQqPsX7qsWAKVFDuPAeEE4m+lac3ROrA3FU7ulXGXqfklbmqM7i7lLV4E3/pnZynbcluOMEhDox8fiNdWyNOmaCt/teD4Z\"\n",
        "    \"hPGSX5gAGqkus808H6oUN0WjivjAYOoTcNeZTsPDtlNS7MG7gLnjcqR7HffRPd5iRqCjNgkOM33DMxgFf+8VbnXqxftAvMmnik0JurBciIeGyuxD\"\n",
        "    \"BSt6NMcwxfzLvdW6uOa0IsyUlesY2D0FZySB9hCZUOVOmWDqnFe5xZfAy45NSciVoasviq6OgsDvSFXy0UxSAOsNYaJYSXLIxWNML5TciAq8bBPK\"\n",
        "    \"aBRFvWQs5VLUIKMXOLjjudxJDHQlrwpqD6Zys1QvXBzvo7aI1qObb2qs1Evmbm2Cto6uGdLMMkVyxOAwh5Z/RkOnRw5aepK95ld7wfzKALMC/opH\"\n",
        "    \"2zivwl/nZv80dbnYOd2ZU93kHtvb+Pa2RwVPSfn4H2J/v9T8OtypDSyie6lcfzexHnmMabrjFyVMm/q2ewPEgW+ScUiafKV7LjarVRg+H1FtOVEo\"\n",
        "    \"YhYu0sKeOOdWUJx+VlZ0NopxwArKfHbpHvW7DyLvCIyGdi6OdDDlYYyvbQ5VguPyfCTOT0fi1bk+ad6s1yko314Oa0eEhmr18+k5J0HMnFiiGqyB\"\n",
        "    \"fGnReTLpFvt0foLPr7rAzhAY4eteLesTKIrXErhVVZd2k1BqYZh24U8YGpI18GuVQlZlmDkhqLhO+oDKHxQkAoBfnbzJKr2SK0yKnI9fnf8Ha46x\"\n",
        "    \"efWPLG7lqqykIz9gV8Fo8tZz6Kjec+SaW7pNCn1MSoVeicSXLHc4JqhtJpiK//+Lmmq5olKPBMtDp6fx5GCf46cSS534zIGN2rOTsWPP50syd3rl\"\n",
        "    \"16vyCq9+2UXySW3cvfLpYUSRsPO9e+VC3eg8jV+9spVA+gi8n6vZl4+BUf9+Amb4VQ8HeBoOSld4QW4q9NsYHAOEjggFT652Z5PoGgz/lQ0wiCFO\"\n",
        "    \"u8crWASSFxsbyOh6NWdRemFYt8UGM5xgft/fHDx29Zqi3ky8hqdsKGF6rGaL9i3YDveVItIBFZutFdwDZIe55Vy8h8uj70vszJ8N5wZH6pCxJ8Zu\"\n",
        "    \"Gk2J7EACzdsbk4RRCSZlCL10m+n0YJO9qE4pQ8AjEoyFLixYVuf8iKIz99lOhR6w3Hc6E4M4cHeMzThPl2RUOuIiGu6BFomTYXjKqwz7zMiVAR2u\"\n",
        "    \"jTrWYzCJtT8m/xnfA/ceM/rqfUr0jrgmoUo157U2p6dUb6BmS7OEXiCnCoacdyzpPpZ/en3ODCCaJEEOzudY2XmBloRmYxtPgdXA8+f/1suU3JVh\"\n",
        "    \"7tH9vkMZT6R7A8UD1yMCZzr9tl3vTepRB/Oi3bZgGAkdLjKgDgURTqfJRpH94T0Koz7vtpl0Q3kF8fitJO+1WzhNq55abqIzDE4WWDv7SWkaxWWi\"\n",
        "    \"gV/A6X9PN3w/yX0zIb3sprS3zsFnajb44c2pCuz/ZlUUOlQYdetDei4HlPcp1wk5ykztumjAZ1tRPSizGmJCM8fiI0o0FxDT5YhVjoVNsJySemyZ\"\n",
        "    \"bTl44csy8n4usWa66Lz6QZ9J73D3v6CKlk+L3fpZht25GquPklF/8HHv3juy7CJo/a/2Vx0v3S2lfnWkfY0lv/SxRv9AHSAcGKwbeWXWDpv8usAz\"\n",
        "    \"abrc7CKp7uBhYFyVeCJSqL74DgvanwHZwcBiyqAgKkKqhp13u+ifZ4hrXEC8na7wbjODHJBlByRo3R0zq3UxP0z12rhzR90oCB3Z1CdzIIx5YesK\"\n",
        "    \"qBtHCZHhDNPrqZpoLWWZQswMpCL3Quu5b+yrDJQ4Xm0y8Nh0NTipAbsqYi332tzgMilyZGbVa+7pkygyViQvrgfKEAZCOOQtCU5sq0LnXpfZ7ume\"\n",
        "    \"iOkD05XnobDTvi5lshMwyljEMbUSzydnIl9BqVGM0EJF5ROzSRHI0C2pz4NgYQIULzh4JOJahJ306JU9qI0gD4g/GjOiLNGF0FZT4+k8US2dSBOk\"\n",
        "    \"dp0miuFV/YGtumkzZnJn5SORZeUCayk7kGgd9pifvkadKNc77zf8r2wG5ocbE5vWiFGW4+un1nlB7wtxckM7U+jLhBX+NHyBYetXB92zFF5VZVKs\"\n",
        "    \"pyG9hXIkXpyeIwQIfw8B8v92ruj+MAc1Gd54By4358QN6MLmi+Lnj7iTWKN+h7pIJdQxVCrVtXf3rBJL9pWZpq0UG7BwNlkPSgoNuVMLSFUBy7Js\"\n",
        "    \"1CtYMro74+T+1WEAGGL9Tt5M2Rs0HfSGuFuVqWiXKd/XWEsM6vNmjbO14PHF/CqpRd626kIQgMfErL4O1AvuFflMyNFkw6/aNG948gjsVgFRw/Dg\"\n",
        "    \"fnxeLSnnq/nRf0+j5Tbs5PNeJ57lvOeOaLZ6O62WI/9NqlMLrnMBH29pqXLzw47K++nZ/lkox7KHVjt8aVJWE8yv3hA4rkLS0Zpi+nq1Dfv2Bt3B\"\n",
        "    \"uh18kQH+KE+Oxl/6NRczcu4GxtCudPMZ+kd5m1MF8tLWjbj1HbNelbb7gxKqMsJtnW/w7Dafq0pOhg8aZdIfB+481e5zH+0DUKH8WDeqFmzsEwNC\"\n",
        "    \"DkyVbVXe+gr4AkEiJPjvdEZ3XfBCy4lRSj0gwFxXdKBGMKhs08A9Pj4DY+wBnWigANOXswh699fYyTi8xWzrsvcq3QtHpLhm7cuqgfyfAD28ROvN\"\n",
        "    \"xOrNwBYs8aK1L/gkQH6R8oEw84NAIo6G0lybNADV9NCoPurTP7zTRCqMX37XeznxkGztpkwP3b1rHj2J/2xv/mSk8Z+q31GMYq9Ub7gHTbBxsCh0\"\n",
        "    \"ppSOUN4HWC0EUG+KsNy01aYFV6QesbrpntC76Mz2Zsq7RNl5CO8Wk+mjfLcNo54Xp/13dejwHoWcjxu1Q6DODiw4J4o0jV1we/0KTByQEW46bsZr\"\n",
        "    \"op19Ay6VB5g3rjAJ1V800O8T/UYU+I5GsPAgt3VDF4l+h/7glkBnk7DnK0UlnSfh3xFw9ibyO8TrG/iM2VZ0PFSuSN6Dp5KUN47dxmL8weImNpba\"\n",
        "    \"wgzUMekkAOZ78dIohPXgquCBkqz1zc8gvZoDN10v899vVuuirD7XTbu5vbvf/nE6OXv+4vzlV1+/SgLlOunBMJDy6k8Vnig6M6YFVWlRAQrQG8tP\"\n",
        "    \"ZD3HoHElW842Z/l13jZ8wRZ2FNrmZS3d3BcSg4pONZVPhH4lfDxvbu11Cra6YMKgNcRRIz5Z0TLkAjTqGr/oqvYCVH3Tcj2c/rMVCe486yr1txYO\"\n",
        "    \"Kg/uK8Yna5Sfrkvuw+xIJNodv6VTAgoS2ukDLQNwPcHEMMn9jnWgHTHFouxO0wDUwViMWRa5iE4khiuhA1YbwYUqtlAQ1F9q6JcEhLZW5eExMsUN\"\n",
        "    \"A4CdtAyZ4B3nFwMkYqVzOEr+feZ9OGkNptHR33cjAzxYr0tdjsh18TB66I9wDIxWLE7kDaptuyR2138GJeYWLQhoCAJyEjltkOgHSfKUN8B/GYWP\"\n",
        "    \"ot1hyG85weM/juI8GyIP6z7ofYnCG5Ou43vOHLiQfM8ejSbtKI8AzGeMfwgmMD3iuxovMLfyvg3xSZzB+ppQawTWI0U7PRtxsVNSmNeQgesY/Dea\"\n",
        "    \"bIgQS7LYwaZdjL/232pyeUxIjWi22dH/AVBLAQIUABQAAAAIAAAAIQC4SZApESQAAOFxAAARAAAAAAAAAAAAAACAAQAAAABmZTNfY3FkX3NlbnNv\"\n",
        "    \"ci5weVBLAQIUABQAAAAIAAAAIQByARRnlx8AACFoAAAQAAAAAAAAAAAAAACAAUAkAABjcWRfc2ltdWxhdG9yLnB5UEsFBgAAAAACAAIAfQAAAAVE\"\n",
        "    \"AAAAAA==\"\n",
        ")\n",
        "module_directory = Path(tempfile.mkdtemp(prefix=\"cqd-notebook-\"))\n",
        "with zipfile.ZipFile(io.BytesIO(base64.b64decode(BUNDLED_SOURCE_ZIP))) as archive:\n",
        "    if set(archive.namelist()) != set(BUNDLED_SHA256):\n",
        "        raise RuntimeError(\"Unexpected notebook source files\")\n",
        "    for name, expected in BUNDLED_SHA256.items():\n",
        "        data = archive.read(name)\n",
        "        if hashlib.sha256(data).hexdigest() != expected:\n",
        "            raise RuntimeError(f\"Source checksum mismatch: {name}\")\n",
        "        (module_directory / name).write_bytes(data)\n",
        "sys.path.insert(0, str(module_directory))\n",
        "# Reload this notebook's snapshot if setup is run again in the same kernel.\n",
        "for name in (\"cqd_simulator\", \"fe3_cqd_sensor\"):\n",
        "    sys.modules.pop(name, None)\n",
        "\n",
        "from dataclasses import asdict, replace\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import matplotlib.pyplot as plt\n",
        "import fe3_cqd_sensor as core\n",
        "import cqd_simulator as sim\n",
        "from IPython.display import display\n",
        "\n",
        "plt.rcParams.update({\"figure.dpi\": 110, \"axes.spines.top\": False,\n",
        "                     \"axes.spines.right\": False, \"axes.grid\": True,\n",
        "                     \"grid.alpha\": 0.18})\n",
        "print(f\"Simulator {sim.VERSION}; bundled source checksums verified.\")"
      ],
      "id": "cqd-lab-01",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. Choose a hypothetical batch and instrument\n",
        "\n",
        "Site concentration is a **bulk effective binding capacity**, not the CQD\n",
        "particle concentration. The original synthetic population supplies relative\n",
        "class capacities, brightness, and heuristic affinities. The scale factors\n",
        "below remain unfitted. Cu\u00b2\u207a competes and quenches, but its absorption is\n",
        "assumed zero unless you supply an `AbsorptionSpectrum` in Python.\n",
        "\n",
        "F\u2080 is the CQD-only blank at the same pH; a mixture at zero added Fe can\n",
        "therefore have F/F\u2080 below one. The default forward scans omit iron\n",
        "speciation. Section 5 explicitly examines how that assumption can fail."
      ],
      "id": "cqd-lab-02"
    },
    {
      "cell_type": "code",
      "metadata": {
        "cellView": "form"
      },
      "source": [
        "# @title Batch, environment, and instrument controls\n",
        "seed = 42 # @param {type:\"integer\"}\n",
        "population_size = 256 # @param {type:\"integer\"}\n",
        "pH = 7.0 # @param {type:\"number\"}\n",
        "excitation_nm = 365.0 # @param [365.0, 405.0] {type:\"raw\"}\n",
        "pathlength_cm = 1.0 # @param {type:\"number\"}\n",
        "sites_uM = 1.0 # @param {type:\"number\"}\n",
        "copper_uM = 2.0 # @param {type:\"number\"}\n",
        "kd_scale = 1.0 # @param {type:\"number\"}\n",
        "ksv_scale = 1.0 # @param {type:\"number\"}\n",
        "ife_scale = 1.0 # @param {type:\"number\"}\n",
        "lifetime0_ns = 5.0 # @param {type:\"number\"}\n",
        "photons_blank = 100000.0 # @param {type:\"number\"}\n",
        "read_noise_counts = 30.0 # @param {type:\"number\"}\n",
        "background_counts = 200.0 # @param {type:\"number\"}\n",
        "scenario_draws = 24 # @param {type:\"integer\"}\n",
        "\n",
        "population = core.build_population(population_size, seed=seed)\n",
        "conditions = sim.Conditions(pH=pH, excitation_nm=excitation_nm,\n",
        "                            pathlength_cm=pathlength_cm, sites_uM=sites_uM)\n",
        "parameters = sim.Parameters(kd_scale=kd_scale, ksv_scale=ksv_scale,\n",
        "                            ife_scale=ife_scale, lifetime0_ns=lifetime0_ns)\n",
        "noise = sim.NoiseModel(photons_blank=photons_blank, read_noise_counts=read_noise_counts,\n",
        "                       background_counts=background_counts)\n",
        "concentrations = np.r_[0., np.geomspace(.01, 100, 32)]\n",
        "background = {\"Cu2\": copper_uM}\n",
        "spectra = {}  # Optional: {\"Cu2\": sim.AbsorptionSpectrum((200, 800), (..., ...), \"source\")}\n",
        "scenario_seed, observation_seed = seed + 1, seed + 2\n",
        "print(\"Synthetic population:\", len(population), \"classes; settings ready.\")"
      ],
      "id": "cqd-lab-03",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Competing quenchers in one cuvette\n",
        "\n",
        "All site classes share one free pool per analyte. Occupancies compete\n",
        "through a common denominator and the solver enforces each analyte's\n",
        "mass balance. Binding can displace Fe while Cu also quenches: total\n",
        "fluorescence need not increase when Fe binding decreases."
      ],
      "id": "cqd-lab-04"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "pure = sim.concentration_scan(population, concentrations, conditions=conditions,\n",
        "                              parameters=parameters, spectra=spectra)\n",
        "mixture = sim.concentration_scan(population, concentrations, conditions=conditions,\n",
        "                                 parameters=parameters, background=background, spectra=spectra)\n",
        "example = sim.simulate(population, {\"Fe3\": 1., **background}, conditions, parameters, spectra)\n",
        "display(example[\"mass_balance\"])\n",
        "errors = mixture.filter(like=\"mass_error\").abs().to_numpy()\n",
        "print(f\"Maximum absolute mass-balance residual: {errors.max():.2e} \u00b5M\")\n",
        "print(\"Absorption assumptions:\", example[\"optical_sources\"])\n",
        "\n",
        "fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), constrained_layout=True)\n",
        "axes[0].plot(concentrations, pure.ratio, label=\"Fe only\", linewidth=2)\n",
        "axes[0].plot(concentrations, mixture.ratio, label=f\"Fe + {copper_uM:g} \u00b5M Cu\", linewidth=2)\n",
        "axes[0].set(ylabel=\"Fluorescence F/F\u2080\", title=\"Combined optical response\")\n",
        "axes[1].plot(concentrations, pure.Fe3_bound_uM, label=\"Fe bound, alone\", linewidth=2)\n",
        "axes[1].plot(concentrations, mixture.Fe3_bound_uM, label=\"Fe bound, mixture\", linewidth=2)\n",
        "axes[1].set(ylabel=\"Bound Fe (\u00b5M)\", title=\"Competition for finite capacity\")\n",
        "for ax in axes:\n",
        "    ax.set(xscale=\"symlog\", xlabel=\"Total added Fe (\u00b5M)\")\n",
        "    ax.set_xscale(\"symlog\", linthresh=.02)\n",
        "    ax.legend(fontsize=9)\n",
        "plt.show()"
      ],
      "id": "cqd-lab-05",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Compare mathematical hypotheses\n",
        "\n",
        "Toggle optical mechanisms while holding binding equilibria and parameters\n",
        "fixed. Even \"IFE only\" retains binding for mass balance; its bound species\n",
        "are assumed as emissive as unbound species. These are controlled model\n",
        "comparisons, not fitted competing theories.\n",
        "\n",
        "Dynamic quenching shortens each class's lifetime. Static quenching and\n",
        "attenuation do not shorten it in this model. The plotted ensemble lifetime\n",
        "is an intensity-weighted mean, not a simulated TCSPC decay or an instrument\n",
        "fit. A common unquenched lifetime is assumed for every class."
      ],
      "id": "cqd-lab-06"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "hypotheses = sim.compare_hypotheses(population, concentrations, conditions=conditions,\n",
        "                                     parameters=parameters, background=background, spectra=spectra)\n",
        "fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), constrained_layout=True)\n",
        "for name, group in hypotheses.groupby(\"hypothesis\", sort=False):\n",
        "    axes[0].plot(group.concentration_uM, group.ratio, label=name, linewidth=2)\n",
        "    axes[1].plot(group.concentration_uM, group.lifetime_ns, label=name, linewidth=2)\n",
        "axes[0].set(ylabel=\"Fluorescence F/F\u2080\", title=\"Intensity alone can be ambiguous\")\n",
        "axes[1].set(ylabel=\"Mean lifetime (ns)\", title=\"A second observable tests assumptions\")\n",
        "for ax in axes:\n",
        "    ax.set_xscale(\"symlog\", linthresh=.02)\n",
        "    ax.set_xlabel(\"Total added Fe (\u00b5M)\")\n",
        "axes[0].legend(fontsize=8)\n",
        "plt.show()"
      ],
      "id": "cqd-lab-07",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. Separate parameter uncertainty from instrument noise\n",
        "\n",
        "Independent log-uniform ranges below are **chosen assumptions**, centered\n",
        "on your controls. They are not inferred from data. The 5\u201395% envelope holds\n",
        "the synthetic population fixed; changing the population seed is a separate\n",
        "sensitivity check. Instrument draws use Poisson photon noise, Gaussian\n",
        "read noise, and a shared measured blank denominator. The background mean\n",
        "is assumed known, although background shot noise remains.\n",
        "\n",
        "Edit these prior ranges to ask how strongly a conclusion depends on an\n",
        "uncertain affinity, quenching coefficient, absorption, or site capacity."
      ],
      "id": "cqd-lab-08"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "priors = {\"kd_scale\": sim.ParameterRange(kd_scale / 3, kd_scale * 3),\n",
        "          \"ksv_scale\": sim.ParameterRange(ksv_scale / 2, ksv_scale * 2, ksv_scale > 0),\n",
        "          \"ife_scale\": sim.ParameterRange(ife_scale / 2, ife_scale * 2, ife_scale > 0),\n",
        "          \"sites_uM\": sim.ParameterRange(sites_uM / 3, sites_uM * 3, sites_uM > 0)}\n",
        "envelope = sim.uncertainty_scan(population, concentrations, priors, n_draws=scenario_draws,\n",
        "                                seed=scenario_seed, conditions=conditions, parameters=parameters,\n",
        "                                background=background, spectra=spectra)\n",
        "observation = sim.observe(mixture, noise, seed=observation_seed)\n",
        "sensitivity = sim.sensitivity_scan(population, 1., conditions=conditions, parameters=parameters,\n",
        "                                    background=background, spectra=spectra)\n",
        "print(envelope[\"label\"])\n",
        "print(f\"Synthetic blank SD in F/F\u2080 units: {observation['sigma_blank_ratio']:.4g}\")\n",
        "display(sensitivity)\n",
        "\n",
        "# A compact overview is also exported for sharing with its synthetic label intact.\n",
        "fig_overview, axes = plt.subplots(2, 2, figsize=(11, 8), constrained_layout=True)\n",
        "for name, group in hypotheses.groupby(\"hypothesis\", sort=False):\n",
        "    axes[0, 0].plot(group.concentration_uM, group.ratio, label=name, linewidth=2)\n",
        "    axes[0, 1].plot(group.concentration_uM, group.lifetime_ns, linewidth=2)\n",
        "axes[0, 0].set(title=\"Mechanism comparisons\", ylabel=\"Fluorescence F/F\u2080\")\n",
        "axes[0, 0].legend(fontsize=8)\n",
        "axes[0, 1].set(title=\"Lifetime adds another constraint\", ylabel=\"Mean lifetime (ns)\")\n",
        "band = envelope[\"summary\"]\n",
        "axes[1, 0].fill_between(band.concentration_uM, band.q05, band.q95, alpha=.2,\n",
        "                        label=\"5\u201395% prior scenarios\")\n",
        "axes[1, 0].plot(concentrations, mixture.ratio, color=\"#14665f\", label=\"Chosen parameters\")\n",
        "readings = observation[\"readings\"]\n",
        "axes[1, 0].scatter(readings.concentration_uM, readings.ratio_observed, s=9,\n",
        "                   alpha=.55, color=\"#de7c29\", label=\"Simulated readings\")\n",
        "axes[1, 0].set(title=\"Parameter range \u2260 measurement noise\", ylabel=\"Fluorescence F/F\u2080\")\n",
        "axes[1, 0].legend(fontsize=8)\n",
        "colors = [\"#14665f\" if value >= 0 else \"#de7c29\" for value in sensitivity.effect]\n",
        "axes[1, 1].barh(sensitivity.parameter, sensitivity.effect, color=colors)\n",
        "axes[1, 1].axvline(0, color=\"black\", linewidth=.6)\n",
        "axes[1, 1].set(title=\"Local sensitivity at 1 \u00b5M Fe\", xlabel=\"F/F\u2080(+10%) \u2212 F/F\u2080(\u221210%)\")\n",
        "for ax in (axes[0, 0], axes[0, 1], axes[1, 0]):\n",
        "    ax.set_xscale(\"symlog\", linthresh=.02)\n",
        "    ax.set_xlabel(\"Total added Fe (\u00b5M)\")\n",
        "fig_overview.suptitle(\"CQD hypothesis laboratory | SYNTHETIC \u00b7 UNCALIBRATED\", fontsize=14)\n",
        "plt.show()"
      ],
      "id": "cqd-lab-09",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Stress-test the soluble-iron assumption\n",
        "\n",
        "This optional model uses **mononuclear Fe(III) hydrolysis, ideal activities,\n",
        "fixed pH and 25 \u00b0C**, plus equilibrium with amorphous Fe(OH)\u2083. Only aquo\n",
        "Fe\u00b3\u207a binds and dynamically quenches here. It omits buffers, ligand\n",
        "complexation, oligomers, redox, other solids and precipitation kinetics.\n",
        "It is a limited chemical scenario, not a full aqueous-speciation model.\n",
        "\n",
        "Hydrolysis constants: cumulative log K = \u22122.19, \u22125.67, \u221212.56, \u221221.6;\n",
        "amorphous solid dissolution log K = 4.891. See the\n",
        "[PHREEQC database](https://github.com/phreeqc-dev/phreeqc3/blob/master/database/phreeqc.dat)\n",
        "and [USGS manual, database appendix](https://pubs.usgs.gov/wri/1995/4227/report.pdf).\n",
        "These constants do not calibrate CQD binding or optical response.\n",
        "\n",
        "A predicted solid makes optical results unavailable: its scattering,\n",
        "adsorption and geometry are unspecified. A diagnostic dissolved-only\n",
        "counterfactual is returned by the API, but is not a suspension prediction."
      ],
      "id": "cqd-lab-10"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "chemistry_rows = []\n",
        "chemistry_total_uM = 10.\n",
        "for chemistry_pH in np.linspace(2, 9, 36):\n",
        "    chemical_conditions = replace(conditions, pH=float(chemistry_pH),\n",
        "                                    fe_speciation=True, precipitation=True)\n",
        "    result = sim.simulate(population, {\"Fe3\": chemistry_total_uM}, chemical_conditions, parameters)\n",
        "    balance = result[\"mass_balance\"].iloc[0]\n",
        "    chemistry_rows.append({\"pH\": chemistry_pH, **result[\"speciation\"][\"fractions\"],\n",
        "                            \"free_uM\": balance.free_uM, \"bound_uM\": balance.bound_uM,\n",
        "                            \"solid_uM\": balance.precipitated_uM,\n",
        "                            \"optics_valid\": result[\"optics_valid\"]})\n",
        "chemistry = pd.DataFrame(chemistry_rows)\n",
        "fig, axes = plt.subplots(1, 2, figsize=(11, 3.8), constrained_layout=True)\n",
        "for species in sim.FE_SPECIES:\n",
        "    axes[0].plot(chemistry.pH, chemistry[species], label=species)\n",
        "axes[0].set(xlabel=\"Fixed pH\", ylabel=\"Fraction of unbound dissolved Fe\",\n",
        "            title=\"Ideal hydrolysis scenario\")\n",
        "axes[0].legend(fontsize=8)\n",
        "axes[1].stackplot(chemistry.pH, chemistry.free_uM, chemistry.bound_uM, chemistry.solid_uM,\n",
        "                  labels=[\"Unbound dissolved\", \"CQD-bound\", \"Solid\"], alpha=.8)\n",
        "axes[1].set(xlabel=\"Fixed pH\", ylabel=\"Fe inventory (\u00b5M)\", title=\"10 \u00b5M total Fe; mass conserved\")\n",
        "axes[1].legend(fontsize=8)\n",
        "plt.show()\n",
        "print(f\"Solid predicted at {(~chemistry.optics_valid).sum()} / {len(chemistry)} conditions;\",\n",
        "      \"fluorescence is not reported for those conditions.\")"
      ],
      "id": "cqd-lab-11",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Where do the hypotheses disagree most?\n",
        "\n",
        "This ranks a small grid by mean pairwise separation in assumed intensity\n",
        "and lifetime noise units. It is a **fixed-parameter screening heuristic**,\n",
        "not formal optimal design, statistical evidence or proof that a condition\n",
        "is experimentally feasible. A small minimum pairwise score means at least\n",
        "two hypotheses remain difficult to distinguish. All hypotheses would need\n",
        "fitting and uncertainty assessment before a serious experiment-design claim.\n",
        "\n",
        "This grid retains the main scan's soluble-Fe assumption. Apply chemical\n",
        "feasibility checks before selecting actual pH/concentration conditions."
      ],
      "id": "cqd-lab-12"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "ranking_concentrations = [.1, .5, 1., 5., 20.]\n",
        "ranking_ph = [4., 7.]\n",
        "ranking_wavelengths = [365., 405.]\n",
        "ranking_ratio_sd, ranking_lifetime_sd_ns = .01, .1\n",
        "candidates = sim.rank_discriminating_conditions(\n",
        "    population, ranking_concentrations, ph_values=ranking_ph,\n",
        "    wavelengths_nm=ranking_wavelengths, conditions=conditions, parameters=parameters,\n",
        "    ratio_sd=ranking_ratio_sd, lifetime_sd_ns=ranking_lifetime_sd_ns,\n",
        "    background=background, spectra=spectra)\n",
        "display(candidates.head(10))"
      ],
      "id": "cqd-lab-13",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 7. Export a reproducible run\n",
        "\n",
        "Exports include the population, response and mass-balance columns, hypothesis\n",
        "curves, parameter draws, uncertainty summaries, synthetic replicate readings,\n",
        "blank counts, chemistry scenarios, ranking and settings. The JSON records\n",
        "seeds, software versions, model-source hashes, assumptions and noise settings.\n",
        "Only these selected outputs enter the download ZIP. Re-running overwrites\n",
        "this run folder; rename it if you want to preserve multiple runs."
      ],
      "id": "cqd-lab-14"
    },
    {
      "cell_type": "code",
      "metadata": {
        "cellView": "form"
      },
      "source": [
        "# @title Save outputs (optional Colab download)\n",
        "download_results = False # @param {type:\"boolean\"}\n",
        "output_directory = Path(\"simulation_outputs/notebook_run\")\n",
        "scenario_curves = pd.DataFrame(envelope[\"curves\"].T,\n",
        "                               columns=[f\"draw_{i}\" for i in range(scenario_draws)])\n",
        "scenario_curves.insert(0, \"concentration_uM\", concentrations)\n",
        "tables = {\"population\": population, \"fe_only\": pure, \"mixture\": mixture,\n",
        "          \"hypotheses\": hypotheses, \"uncertainty\": envelope[\"summary\"],\n",
        "          \"parameter_draws\": envelope[\"draws\"], \"scenario_curves\": scenario_curves,\n",
        "          \"simulated_measurements\": observation[\"readings\"],\n",
        "          \"blank_counts\": pd.DataFrame({\"counts\": observation[\"blank_counts\"]}),\n",
        "          \"sensitivity\": sensitivity, \"iron_chemistry\": chemistry, \"candidate_conditions\": candidates}\n",
        "settings = {\"source_sha256\": BUNDLED_SHA256, \"scenario_draws\": scenario_draws,\n",
        "            \"concentrations_uM\": concentrations.tolist(), \"sensitivity_concentration_uM\": 1.,\n",
        "            \"sensitivity_relative_step\": .1, \"chemistry_total_uM\": chemistry_total_uM,\n",
        "            \"chemistry_ph_grid\": chemistry.pH.tolist(),\n",
        "            \"chemistry_flags\": {\"fe_speciation\": True, \"precipitation\": True},\n",
        "            \"ranking_concentrations_uM\": ranking_concentrations, \"ranking_ph\": ranking_ph,\n",
        "            \"ranking_wavelengths_nm\": ranking_wavelengths, \"ranking_ratio_sd\": ranking_ratio_sd,\n",
        "            \"ranking_lifetime_sd_ns\": ranking_lifetime_sd_ns,\n",
        "            \"baseline_counts\": observation[\"baseline_counts\"],\n",
        "            \"sigma_blank_ratio\": observation[\"sigma_blank_ratio\"],\n",
        "            \"optical_sources\": example[\"optical_sources\"],\n",
        "            \"matplotlib_version\": __import__(\"matplotlib\").__version__}\n",
        "exported = sim.export_run(output_directory, tables, conditions=conditions, parameters=parameters,\n",
        "                           population_seed=seed, population_size=population_size, noise=noise,\n",
        "                           priors=priors, scenario_seed=scenario_seed, observation_seed=observation_seed,\n",
        "                           background=background, spectra=spectra, settings=settings)\n",
        "overview_path = output_directory / \"simulator_overview.png\"\n",
        "fig_overview.savefig(overview_path, dpi=160)\n",
        "exported.append(overview_path)\n",
        "zip_path = output_directory.parent / \"cqd_simulation_run.zip\"\n",
        "with zipfile.ZipFile(zip_path, \"w\", compression=zipfile.ZIP_DEFLATED) as archive:\n",
        "    for path in exported:\n",
        "        archive.write(path, arcname=path.name)\n",
        "print(\"Saved:\", zip_path.resolve())\n",
        "if download_results:\n",
        "    try:\n",
        "        from google.colab import files\n",
        "    except ImportError:\n",
        "        print(\"Local runtime: open the ZIP path printed above.\")\n",
        "    else:\n",
        "        files.download(str(zip_path))"
      ],
      "id": "cqd-lab-15",
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## What this can and cannot establish\n",
        "\n",
        "Use these calculations to reject internally inconsistent assumptions,\n",
        "inspect confounding mechanisms, and decide what to measure. A visually\n",
        "convincing curve is not evidence that the chosen parameters describe your batch.\n",
        "\n",
        "To connect this notebook to a real batch: measure replicate blanks,\n",
        "fluorescence titration, absorbance and preferably lifetime under recorded\n",
        "solution conditions; fit only identifiable parameters; then test **new\n",
        "preparations or held-out samples** without refitting. Synthetic noise draws\n",
        "and mechanism comparisons here are neither experimental fitting nor independent\n",
        "validation. An experimental importer and fitting workflow are separate work.\n",
        "\n",
        "The model remains equilibrium-only. It omits kinetics, photobleaching,\n",
        "aggregation, matrix chemistry, realistic instrument geometry, temperature\n",
        "dependence and species-resolved spectra. The synthetic structure\u2013property\n",
        "mapping is heuristic, not electronic-structure or molecular-dynamics simulation."
      ],
      "id": "cqd-lab-16"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.10"
    },
    "colab": {
      "name": "simulation_lab.ipynb",
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}
