{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/tymworld/DROPPS/blob/v1.0.0/examples/colab/00_DROPPS_1_0_Quickstart.ipynb)\n\n# DROPPS 1.0 Colab Quickstart\n\nThis notebook performs a complete, deliberately tiny workflow: sequence →\ncoarse-grained model → multichain box → portable DROPPS TPR v2 → short\nOpenMM run → validation → density analysis. It is an interface and file-flow\ncheck, not a phase-separation production simulation.\n\nUse the focused notebooks for multicomponent systems, structural restraints,\nGPU/restart controls, trajectory transformations, and manuscript-level analyses.\n"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## Runtime\n\nIn Colab choose **Runtime → Change runtime type → GPU** when testing CUDA.\nThis quickstart defaults to OpenMM's deterministic `Reference` platform so it\nalso works without a GPU.\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "#@title Install DROPPS 1.0\n#@markdown The default route installs the immutable wheel attached to the\n#@markdown public GitHub `v1.0.0` release. Upload remains available for offline use.\ninstallation_source = \"Install from the DROPPS v1.0.0 GitHub release\" #@param [\"Install from the DROPPS v1.0.0 GitHub release\", \"Upload the DROPPS 1.0 wheel\", \"Install from another wheel URL\"]\nwheel_url = \"https://github.com/tymworld/DROPPS/releases/download/v1.0.0/dropps-1.0-py3-none-any.whl\" #@param {type:\"string\"}\naccelerator_dependencies = \"auto\" #@param [\"auto\", \"cuda12\", \"cuda13\", \"latest\"]\n\nimport importlib.metadata\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom pathlib import Path\n\n\ndef detect_install_extra():\n    if accelerator_dependencies != \"auto\":\n        return accelerator_dependencies\n    if shutil.which(\"nvidia-smi\"):\n        probe = subprocess.run(\n            [\"nvidia-smi\"], text=True, capture_output=True, check=False\n        ).stdout\n        match = re.search(r\"CUDA Version:\\s*(\\d+)\", probe)\n        if match and int(match.group(1)) >= 13:\n            return \"cuda13\"\n        return \"cuda12\"\n    return \"latest\"\n\n\nif installation_source.startswith(\"Upload\"):\n    try:\n        from google.colab import files\n    except ImportError as exc:\n        raise RuntimeError(\n            \"This upload form is intended for Google Colab. Set installation_source \"\n            \"to the URL option when running elsewhere.\"\n        ) from exc\n    uploaded = files.upload()\n    wheel_candidates = [Path(name) for name in uploaded if name.endswith(\".whl\")]\n    if len(wheel_candidates) != 1:\n        raise ValueError(\"Upload exactly one DROPPS 1.0 .whl file.\")\n    wheel_target = str(wheel_candidates[0].resolve())\nelse:\n    if not wheel_url.strip():\n        raise ValueError(\"Provide the published DROPPS 1.0 wheel URL.\")\n    wheel_target = wheel_url.strip()\n\nextra = detect_install_extra()\nif wheel_target.startswith((\"https://\", \"http://\")):\n    package_spec = f\"dropps[{extra}] @ {wheel_target}\"\nelse:\n    package_spec = f\"{wheel_target}[{extra}]\"\nprint(f\"Installing {package_spec}\")\nsubprocess.run(\n    [\n        sys.executable,\n        \"-m\",\n        \"pip\",\n        \"install\",\n        \"--quiet\",\n        \"--upgrade\",\n        \"--upgrade-strategy\",\n        \"only-if-needed\",\n        package_spec,\n    ],\n    check=True,\n)\n\nversion = importlib.metadata.version(\"dropps\")\nif version != \"1.0\":\n    raise RuntimeError(f\"Expected DROPPS 1.0, but installed {version}.\")\nsubprocess.run([\"dps\", \"--version\"], check=True)\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "#@title Shared notebook helpers\nimport hashlib\nimport json\nimport os\nimport shlex\nimport subprocess\nimport zipfile\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\n\nCOMMAND_LOG = []\n\n\ndef run_command(arguments, *, cwd=None, input_text=None, check=True):\n    command = [str(value) for value in arguments]\n    print(\"$\", shlex.join(command))\n    result = subprocess.run(\n        command,\n        cwd=None if cwd is None else str(cwd),\n        input=input_text,\n        text=True,\n        capture_output=True,\n        check=False,\n    )\n    if result.stdout:\n        print(result.stdout, end=\"\" if result.stdout.endswith(\"\\n\") else \"\\n\")\n    if result.stderr:\n        print(result.stderr, end=\"\" if result.stderr.endswith(\"\\n\") else \"\\n\")\n    COMMAND_LOG.append(\n        {\n            \"time_utc\": datetime.now(timezone.utc).isoformat(),\n            \"cwd\": str(Path(cwd or Path.cwd()).resolve()),\n            \"command\": command,\n            \"returncode\": result.returncode,\n        }\n    )\n    if check and result.returncode:\n        raise RuntimeError(\n            f\"Command failed with exit code {result.returncode}: {shlex.join(command)}\"\n        )\n    return result\n\n\ndef dps(*arguments, cwd=None, input_text=None, check=True):\n    return run_command(\n        [\"dps\", *arguments], cwd=cwd, input_text=input_text, check=check\n    )\n\n\ndef reset_task_directory(path):\n    path = Path(path).resolve()\n    if not path.name.startswith(\"dropps_\"):\n        raise ValueError(f\"Refusing to reset unexpected directory: {path}\")\n    if path.exists():\n        import shutil\n\n        shutil.rmtree(path)\n    path.mkdir(parents=True)\n    return path\n\n\ndef sha256(path):\n    digest = hashlib.sha256()\n    with Path(path).open(\"rb\") as stream:\n        for block in iter(lambda: stream.read(1024 * 1024), b\"\"):\n            digest.update(block)\n    return digest.hexdigest()\n\n\ndef safe_extract_zip(archive, destination):\n    destination = Path(destination).resolve()\n    destination.mkdir(parents=True, exist_ok=True)\n    with zipfile.ZipFile(archive) as bundle:\n        for member in bundle.infolist():\n            target = (destination / member.filename).resolve()\n            if destination not in target.parents and target != destination:\n                raise ValueError(f\"Unsafe ZIP member: {member.filename}\")\n        bundle.extractall(destination)\n\n\ndef find_unique(root, basename):\n    matches = [path for path in Path(root).rglob(basename) if path.is_file()]\n    if len(matches) != 1:\n        raise FileNotFoundError(\n            f\"Expected one file named {basename!r} below {root}, found {len(matches)}.\"\n        )\n    return matches[0]\n\n\ndef make_zip(paths, output_path, *, base=None):\n    output_path = Path(output_path)\n    base = Path(base or output_path.parent).resolve()\n    with zipfile.ZipFile(output_path, \"w\", compression=zipfile.ZIP_DEFLATED) as bundle:\n        for source in sorted({Path(path).resolve() for path in paths}):\n            if source == output_path.resolve() or not source.is_file():\n                continue\n            try:\n                arcname = source.relative_to(base)\n            except ValueError:\n                arcname = Path(source.name)\n            bundle.write(source, arcname)\n    print(f\"Wrote {output_path} ({output_path.stat().st_size / 1024:.1f} KiB)\")\n    return output_path\n\n\ndef download(path):\n    try:\n        from google.colab import files\n    except ImportError:\n        print(f\"Result available at {Path(path).resolve()}\")\n    else:\n        files.download(str(path))\n\n\ndef optional_time_arguments(start, end, interval):\n    arguments = []\n    for option, value in ((\"-b\", start), (\"-e\", end), (\"-dt\", interval)):\n        if str(value).strip():\n            arguments.extend([option, str(value).strip()])\n    return arguments\n\n\ndef read_xvg(path):\n    legends = {}\n    metadata = {}\n    rows = []\n    with Path(path).open(encoding=\"utf-8\") as stream:\n        for raw in stream:\n            line = raw.strip()\n            if not line or line.startswith(\"#\"):\n                continue\n            if line.startswith(\"@\"):\n                re_module = __import__(\"re\")\n                match = re_module.search(r's(\\d+)\\s+legend\\s+\"(.*)\"', line)\n                if match:\n                    legends[int(match.group(1)) + 1] = match.group(2)\n                for key, pattern in {\n                    \"title\": r'@\\s+title\\s+\"(.*)\"',\n                    \"xlabel\": r'@\\s+xaxis\\s+label\\s+\"(.*)\"',\n                    \"ylabel\": r'@\\s+yaxis\\s+label\\s+\"(.*)\"',\n                }.items():\n                    label_match = re_module.search(pattern, line)\n                    if label_match:\n                        metadata[key] = label_match.group(1)\n                continue\n            rows.append([float(value) for value in line.split()])\n    data = np.asarray(rows, dtype=float)\n    if data.ndim != 2 or data.shape[1] < 2:\n        raise ValueError(f\"No plottable data in {path}\")\n    return data, legends, metadata\n\n\ndef plot_xvg(path, *, title=None, output=None):\n    data, legends, metadata = read_xvg(path)\n    fig, ax = plt.subplots(figsize=(7.2, 4.2))\n    for column in range(1, data.shape[1]):\n        ax.plot(data[:, 0], data[:, column], label=legends.get(column, f\"series {column}\"))\n    ax.set_title(title or metadata.get(\"title\", Path(path).stem))\n    ax.set_xlabel(metadata.get(\"xlabel\", \"x / time\"))\n    ax.set_ylabel(metadata.get(\"ylabel\", \"value\"))\n    if data.shape[1] > 2:\n        ax.legend(frameon=False, fontsize=8)\n    ax.spines[[\"top\", \"right\"]].set_visible(False)\n    fig.tight_layout()\n    output_path = Path(output) if output is not None else Path(path).with_suffix(\".png\")\n    fig.savefig(output_path, dpi=220, bbox_inches=\"tight\")\n    print(f\"Saved figure: {output_path}\")\n    plt.show()\n    return fig, output_path\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "#@title Configure the tiny demonstration\nsequence = \"FWFWFWFWFWFWFWFW\" #@param {type:\"string\"}\nmolecule_name = \"PEP\" #@param {type:\"string\"}\nforcefield = \"HPS\" #@param [\"HPS\", \"HPST\", \"CALVADOS2\", \"HPSRNA\", \"MPiPi\", \"MPiPi_PTM\"]\nmolecule_count = 4 #@param {type:\"integer\"}\nrandom_seed = 1215 #@param {type:\"integer\"}\nvalidation_steps = 20 #@param {type:\"integer\"}\nvalidation_platform = \"Reference\" #@param [\"Reference\", \"CPU\", \"CUDA\", \"OpenCL\", \"auto\"]\n\nBASE = Path(\"/content\") if Path(\"/content\").is_dir() else Path.cwd()\nWORKDIR = reset_task_directory(BASE / \"dropps_quickstart\")\nprint(\"Working directory:\", WORKDIR)\n"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 1 Build a molecule and a small system\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "dps(\n    \"pdb2dps\", \"-s\", sequence, \"-ff\", forcefield,\n    \"-oc\", f\"{molecule_name}.pdb\", \"-op\", f\"{molecule_name}.itp\",\n    \"-on\", molecule_name, \"--seed\", random_seed,\n    cwd=WORKDIR,\n)\ndps(\n    \"genmesh\", \"-f\", f\"{molecule_name}.pdb\", \"-p\", f\"{molecule_name}.itp\",\n    \"-n\", molecule_count, \"-mesh\", 2, 2, 1, \"-g\", 1,\n    \"-mx\", 4, \"-my\", 4, \"-mz\", 4,\n    \"-oc\", \"system.pdb\", \"-op\", \"system.top\", \"--seed\", random_seed,\n    cwd=WORKDIR,\n)\n"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 2 Compile a portable run input\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "mdp = f\"\"\"# Tiny DROPPS 1.0 validation run\nintegrator = Langevin\ndt = 0.01\nnsteps = {validation_steps}\nfriction = 1.0\nseed = {random_seed}\nminimize = True\nforcetol = 100\nmax-step = 100\nproduction-temperature = 300\ngen-vel = True\ninitial-temperature = 300\nwarming-speed = 1.0\nkeep-warming-trajectory = False\nbondtype = bond\ncomm-mode = Linear\nnstcomm = 2\nvdwtype = pLJ\ncutoff-scheme-lj = static\ncutoff-lj = 1.0\ncutoff-lj-multi = 3.0\nshift-lj = True\ncoulombtype = yukawa\ncutoff-coul = 1.0\nshift-coul = True\nsalt-conc = 0.1\npcoulp = False\nref-P = 1\ntau-P = 5\nnst-xout = 5\nnst-screenlog = 0\nnst-filelog = 5\nnst-energy = 5\nenergy-grps = potentialEnergy,kineticEnergy,totalEnergy,temperature,boxX,boxY,boxZ,volume,density\nnst-stress = 0\n\"\"\"\n(WORKDIR / \"validation.mdp\").write_text(mdp, encoding=\"utf-8\")\ndps(\n    \"grompp\", \"-f\", \"system.pdb\", \"-p\", \"system.top\",\n    \"-m\", \"validation.mdp\", \"-o\", \"validation.tpr\", cwd=WORKDIR,\n)\n"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 3 Run, check, and analyze\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "dps(\n    \"mdrun\", \"-s\", \"validation.tpr\", \"-o\", \"validation\",\n    \"--platform\", validation_platform, \"-cpt\", 0, cwd=WORKDIR,\n)\ndps(\"check\", \"-s\", \"validation.tpr\", \"-f\", \"validation.xtc\", cwd=WORKDIR)\ndps(\n    \"density\", \"-s\", \"validation.tpr\", \"-f\", \"validation.xtc\",\n    \"-o\", \"density.xvg\", \"-selfit\", 0, \"-sel\", 0,\n    \"--center-mode\", \"none\", cwd=WORKDIR,\n)\nplot_xvg(WORKDIR / \"density.xvg\", title=\"Tiny-run mass-density profile\")\n"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 4 Record provenance and download\n"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": "manifest = {\n    \"notebook\": \"00_DROPPS_1_0_Quickstart.ipynb\",\n    \"dropps_version\": \"1.0\",\n    \"purpose\": \"interface and file-flow validation only\",\n    \"parameters\": {\n        \"sequence\": sequence,\n        \"molecule_name\": molecule_name,\n        \"forcefield\": forcefield,\n        \"molecule_count\": molecule_count,\n        \"random_seed\": random_seed,\n        \"validation_steps\": validation_steps,\n        \"validation_platform\": validation_platform,\n    },\n    \"commands\": COMMAND_LOG,\n}\n(WORKDIR / \"manifest.json\").write_text(\n    json.dumps(manifest, indent=2), encoding=\"utf-8\"\n)\noutputs = [path for path in WORKDIR.iterdir() if path.is_file()]\narchive = make_zip(outputs, WORKDIR / \"DROPPS_1_0_quickstart_results.zip\", base=WORKDIR)\ndownload(archive)\n"
  }
 ],
 "metadata": {
  "colab": {
   "provenance": [],
   "gpuType": "T4"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
