{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-00",
   "metadata": {},
   "source": "# Apache Mahout Hackathon: Qumat and QDP in Google Colab\n\nWelcome! This notebook installs Apache Mahout's `qumat` package and walks through the examples from the kickoff talk, so you can run everything in the browser.\n\n## Before you run anything: set the runtime (1 minute)\n\nColab's default runtime is **Python 3.13**, and Mahout 0.6.0 supports **Python 3.10 to 3.12**. Pick the older runtime and a GPU first, otherwise the install fails and the QDP part cannot run.\n\n1. Menu **Runtime > Change runtime type**.\n2. **Runtime version**: choose **2026.07** (Python 3.12.13). Not \"Latest\".\n3. **Hardware accelerator**: choose **T4 GPU**.\n4. Click **Save**. If Colab asks to restart or reconnect, say yes.\n5. Menu **Runtime > Run all**.\n\nThe first cell of Part 0 prints the Python version. It should say `3.12`; if it says `3.13`, step 2 did not stick, so do it again.\n\n**How the notebook is organized**\n\n- Parts 0 to 3: install and the basics. Today is a 3-hour hackathon: spend at most 10 minutes here, then pick a task from Part 7.\n- Parts 4 and 5: optional extras (parameterized circuits, swap test).\n- Part 6: QDP on the GPU. Needs the T4 and the 2026.07 runtime from above.\n- Part 7: the task list.\n\n**Links**\n\n- Website and docs: https://mahout.apache.org\n- Source and issues: https://github.com/apache/mahout\n- Mailing list: dev@mahout.apache.org"
  },
  {
   "cell_type": "markdown",
   "id": "cell-01",
   "metadata": {},
   "source": "## Part 0: Install\n\nApache Mahout ships on PyPI as `qumat`. The current release, 0.6.0, declares **Python 3.10 to 3.12**. Run the next cell first: it tells you which Python this runtime has and what to do if it is newer.\n\n**If it reports Python 3.13 or newer** (the Colab default may have moved past 3.12): the clean fix is `Runtime > Change runtime type > Runtime version` and pick **2026.07** (Python 3.12.13), then `Runtime > Run all` again. Without that, pip silently falls back to the ancient `qumat 0.0.1` and tries to compile old `qiskit-aer` from source, which fails. Step 1 below has a workaround for Qumat only; the QDP wheel (Step 2) exists for 3.10 to 3.12 only."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-02",
   "metadata": {},
   "outputs": [],
   "source": [
    "import platform\n",
    "import sys\n",
    "\n",
    "PY = sys.version_info[:2]\n",
    "print(\"Python\", platform.python_version(), \"on\", platform.system())\n",
    "!nvidia-smi -L 2>/dev/null || echo \"No GPU attached (that is fine for Parts 0-5)\"\n",
    "\n",
    "if PY > (3, 12):\n",
    "    print(\n",
    "        \"\\nThis Python is newer than qumat 0.6.0 declares. Recommended: Runtime > Change runtime type >\"\n",
    "        \"\\nRuntime version > 2026.07 (Python 3.12), then Run all again. Step 1 will otherwise install\"\n",
    "        \"\\nqumat with --ignore-requires-python; Step 2 (the QDP wheel) will be skipped.\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-03",
   "metadata": {},
   "source": "### Step 1: Qumat (pure Python, works everywhere)"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-04",
   "metadata": {},
   "outputs": [],
   "source": [
    "if PY > (3, 12):\n",
    "    # All of qumat 0.6.0's dependencies ship wheels for 3.13; only qumat's own metadata says <3.13.\n",
    "    %pip install -q --ignore-requires-python \"qumat==0.6.0\"\n",
    "else:\n",
    "    %pip install -q qumat"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-05",
   "metadata": {},
   "source": "### Step 2: QDP, the GPU extra\n\n`pip install \"qumat[qdp]\"` adds the `qumat-qdp` package: a prebuilt Linux wheel for Python 3.10 to 3.12 that bundles the Rust engine and CUDA kernels. It installs on any Linux machine, including a CPU-only Colab runtime, but it needs an NVIDIA GPU to actually encode anything (Part 6). There are no macOS, Windows or Python 3.13 wheels, so the cell skips itself in those cases.\n\nChanging QDP's Rust or CUDA code means rebuilding it from source; the steps are in the appendix of `tasks.md`, not needed today."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-06",
   "metadata": {},
   "outputs": [],
   "source": [
    "if platform.system() != \"Linux\":\n",
    "    print(\"qumat-qdp only ships Linux wheels; skipping on\", platform.system())\n",
    "elif PY > (3, 12):\n",
    "    print(\n",
    "        \"qumat-qdp has no wheel for Python\",\n",
    "        platform.python_version(),\n",
    "        \"- pin the runtime version to 2026.07 to use Part 6\",\n",
    "    )\n",
    "else:\n",
    "    %pip install -q \"qumat[qdp]\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-07",
   "metadata": {},
   "source": "If Colab shows a *Restart runtime* button after the installs, click it and continue from the next cell. The packages stay installed."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-08",
   "metadata": {},
   "outputs": [],
   "source": [
    "from importlib.metadata import PackageNotFoundError, version\n",
    "\n",
    "from qumat import QuMat\n",
    "\n",
    "print(\"qumat    \", version(\"qumat\"))\n",
    "try:\n",
    "    print(\n",
    "        \"qumat-qdp\",\n",
    "        version(\"qumat-qdp\"),\n",
    "        \"(GPU extra installed; needs a GPU at run time)\",\n",
    "    )\n",
    "except PackageNotFoundError:\n",
    "    print(\"qumat-qdp not installed (Parts 1-5 do not need it)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-09",
   "metadata": {},
   "source": "## Part 1: Your first circuit, a Bell state\n\n`QuMat` is the one class you need. You give it a backend config (which simulator to use and how many shots), then build the circuit gate by gate.\n\nA Bell state is the \"hello world\" of quantum computing: a Hadamard puts qubit 0 into superposition, and a CNOT entangles qubit 1 with it. Measuring should give only `00` and `11`, each about half the time."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "qiskit_config = {\n",
    "    \"backend_name\": \"qiskit\",\n",
    "    \"backend_options\": {\"simulator_type\": \"aer_simulator\", \"shots\": 1024},\n",
    "}\n",
    "\n",
    "qc = QuMat(qiskit_config)\n",
    "qc.create_empty_circuit(num_qubits=2)\n",
    "qc.apply_hadamard_gate(0)\n",
    "qc.apply_cnot_gate(0, 1)\n",
    "\n",
    "print(qc.draw())\n",
    "counts = qc.execute_circuit()\n",
    "print(counts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-11",
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "plt.bar(counts.keys(), counts.values(), color=\"#F28C28\")\n",
    "plt.title(\"Bell state, 1024 shots\")\n",
    "plt.xlabel(\"measured bit string\")\n",
    "plt.ylabel(\"counts\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-12",
   "metadata": {},
   "source": "**Try it:** add `qc.apply_pauli_x_gate(0)` before executing and see how the histogram changes."
  },
  {
   "cell_type": "markdown",
   "id": "cell-13",
   "metadata": {},
   "source": "## Part 2: Same circuit, three backends\n\nThe backend is just a config dictionary. The same gate calls run on Qiskit Aer, Cirq, or the Amazon Braket local simulator."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-14",
   "metadata": {},
   "outputs": [],
   "source": [
    "def bell(backend_name, simulator_type, shots=100):\n",
    "    qc = QuMat(\n",
    "        {\n",
    "            \"backend_name\": backend_name,\n",
    "            \"backend_options\": {\"simulator_type\": simulator_type, \"shots\": shots},\n",
    "        }\n",
    "    )\n",
    "    qc.create_empty_circuit(2)\n",
    "    qc.apply_hadamard_gate(0)\n",
    "    qc.apply_cnot_gate(0, 1)\n",
    "    return qc.execute_circuit()\n",
    "\n",
    "\n",
    "for name, sim in [\n",
    "    (\"qiskit\", \"aer_simulator\"),\n",
    "    (\"cirq\", \"default\"),\n",
    "    (\"amazon_braket\", \"local\"),\n",
    "]:\n",
    "    print(f\"{name:14s} -> {bell(name, sim)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-15",
   "metadata": {},
   "source": "Notice the result formats differ slightly: Qiskit and Braket return a dict of bit strings, Cirq returns a list with integer basis states (`3` is `11`). A small helper that normalizes these would make a nice hackathon contribution."
  },
  {
   "cell_type": "markdown",
   "id": "cell-16",
   "metadata": {},
   "source": "## Part 3: Look at the state vector\n\nCounts are samples. `get_final_state_vector()` returns the exact amplitudes, which is handy for debugging and for teaching.\n\nFor the Bell state you should see amplitude `1/sqrt(2) ~ 0.707` on `|00>` and `|11>` and zero elsewhere."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-17",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "qc = QuMat(qiskit_config)\n",
    "qc.create_empty_circuit(2)\n",
    "qc.apply_hadamard_gate(0)\n",
    "qc.apply_cnot_gate(0, 1)\n",
    "\n",
    "state = np.asarray(qc.get_final_state_vector())\n",
    "for index, amplitude in enumerate(state):\n",
    "    print(\n",
    "        f\"|{index:02b}>  amplitude = {np.round(amplitude, 3)}   probability = {abs(amplitude) ** 2:.3f}\"\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-18",
   "metadata": {},
   "source": "## Part 4: Parameterized circuits and a tiny optimization\n\nRotation gates accept a **parameter name** instead of a number. You bind values when you execute, which is exactly what variational algorithms (VQE, QAOA, quantum ML) need.\n\nFirst, sweep `theta` on an `Rx` gate. Theory says `P(1) = sin^2(theta / 2)`."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-19",
   "metadata": {},
   "outputs": [],
   "source": [
    "qc = QuMat(qiskit_config)\n",
    "qc.create_empty_circuit(1)\n",
    "qc.apply_rx_gate(0, \"theta\")  # symbolic parameter\n",
    "\n",
    "thetas = np.linspace(0, 2 * np.pi, 13)\n",
    "measured = []\n",
    "for theta in thetas:\n",
    "    counts = qc.execute_circuit(parameter_values={\"theta\": float(theta)})\n",
    "    measured.append(counts.get(\"1\", 0) / 1024)\n",
    "\n",
    "plt.plot(\n",
    "    thetas, np.sin(thetas / 2) ** 2, label=\"theory: sin^2(theta/2)\", color=\"#1FA3A3\"\n",
    ")\n",
    "plt.plot(thetas, measured, \"o\", label=\"measured (1024 shots)\", color=\"#F28C28\")\n",
    "plt.xlabel(\"theta\")\n",
    "plt.ylabel(\"P(1)\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-20",
   "metadata": {},
   "source": "Now let an optimizer drive the parameter. The cost function runs the circuit and returns how far `P(1)` is from a target of 0.25. SciPy's COBYLA is gradient-free, so shot noise is fine."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-21",
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.optimize import minimize\n",
    "\n",
    "TARGET = 0.25\n",
    "history = []\n",
    "\n",
    "\n",
    "def cost(x):\n",
    "    counts = qc.execute_circuit(parameter_values={\"theta\": float(x[0])})\n",
    "    p1 = counts.get(\"1\", 0) / 1024\n",
    "    history.append(p1)\n",
    "    return (p1 - TARGET) ** 2\n",
    "\n",
    "\n",
    "result = minimize(\n",
    "    cost, x0=[0.1], method=\"COBYLA\", options={\"maxiter\": 40, \"rhobeg\": 0.5}\n",
    ")\n",
    "print(f\"best theta = {result.x[0]:.3f}  (theory: {2 * np.arcsin(np.sqrt(TARGET)):.3f})\")\n",
    "print(f\"P(1) at best theta = {history[-1]:.3f}\")\n",
    "\n",
    "plt.plot(history, color=\"#F28C28\")\n",
    "plt.axhline(TARGET, linestyle=\"--\", color=\"#1FA3A3\", label=\"target\")\n",
    "plt.xlabel(\"evaluation\")\n",
    "plt.ylabel(\"P(1)\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-22",
   "metadata": {},
   "source": "## Part 5: Swap test, how similar are two states?\n\nQumat has a built-in swap test. `measure_overlap(q1, q2, ancilla_qubit)` returns the fidelity `|<psi|phi>|^2` between the states on two qubits.\n\n- `|0>` vs `|0>` should give about `1.0`\n- `|0>` vs `|+>` (after a Hadamard) should give about `0.5`\n- `|0>` vs `|1>` should give about `0.0`"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23",
   "metadata": {},
   "outputs": [],
   "source": [
    "def overlap(prepare_second):\n",
    "    qc = QuMat(\n",
    "        {\n",
    "            \"backend_name\": \"qiskit\",\n",
    "            \"backend_options\": {\"simulator_type\": \"aer_simulator\", \"shots\": 4096},\n",
    "        }\n",
    "    )\n",
    "    qc.create_empty_circuit(\n",
    "        3\n",
    "    )  # qubit 0 = ancilla, qubit 1 = |0>, qubit 2 = state under test\n",
    "    prepare_second(qc)\n",
    "    return qc.measure_overlap(1, 2, ancilla_qubit=0)\n",
    "\n",
    "\n",
    "print(\"|0> vs |0> :\", round(overlap(lambda qc: None), 3))\n",
    "print(\"|0> vs |+> :\", round(overlap(lambda qc: qc.apply_hadamard_gate(2)), 3))\n",
    "print(\"|0> vs |1> :\", round(overlap(lambda qc: qc.apply_pauli_x_gate(2)), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-24",
   "metadata": {},
   "source": "## Part 6 (GPU): QDP, encoding classical data on the GPU\n\nQDP (Quantum Data Plane) is the second half of Mahout. It is a Rust engine with CUDA kernels that turns classical data into quantum state vectors directly on the GPU and hands them to PyTorch through DLPack with no host copy.\n\n**This part needs a GPU runtime and a Python 3.10 to 3.12 runtime** (the `qumat-qdp` wheel has no 3.13 build yet). On the free tier pick `Runtime > Change runtime type > T4 GPU` and, if Part 0 reported Python 3.13, `Runtime version > 2026.07`. The cells below skip themselves otherwise."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-25",
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    import torch\n",
    "\n",
    "    HAS_GPU = torch.cuda.is_available()\n",
    "except ImportError:  # torch is preinstalled on Colab; this only matters elsewhere\n",
    "    HAS_GPU = False\n",
    "\n",
    "try:\n",
    "    import _qdp  # the native extension inside the qumat-qdp wheel\n",
    "\n",
    "    HAS_QDP = True\n",
    "except ImportError:\n",
    "    HAS_QDP = False\n",
    "\n",
    "QDP_OK = HAS_GPU and HAS_QDP\n",
    "print(\"CUDA available:\", HAS_GPU, \"| qumat-qdp installed:\", HAS_QDP)\n",
    "if not HAS_GPU:\n",
    "    print(\n",
    "        \"Switch to a GPU runtime and re-run from Part 0 to try QDP. The cells below skip themselves.\"\n",
    "    )\n",
    "elif not HAS_QDP:\n",
    "    print(\n",
    "        \"GPU found, but the qumat-qdp wheel is not installed for Python\",\n",
    "        platform.python_version() + \".\"\n",
    "        \"\\nWheels exist for 3.10 to 3.12 only: Runtime > Change runtime type > Runtime version > 2026.07,\"\n",
    "        \"\\nthen re-run from Part 0. The cells below skip themselves.\",\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-26",
   "metadata": {},
   "source": "QDP itself was installed in Part 0 (`pip install \"qumat[qdp]\"`). Encode four numbers into a 2-qubit state and check the amplitudes against NumPy."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-27",
   "metadata": {},
   "outputs": [],
   "source": [
    "if QDP_OK:\n",
    "    from qumat.qdp import QdpEngine\n",
    "\n",
    "    engine = QdpEngine(device_id=0)  # CUDA backend by default\n",
    "\n",
    "    data = [1.0, 2.0, 3.0, 4.0]\n",
    "    qtensor = engine.encode(data, num_qubits=2, encoding_method=\"amplitude\")\n",
    "\n",
    "    # DLPack hand-off: single use, so convert once and keep the tensor.\n",
    "    state = torch.from_dlpack(qtensor)\n",
    "    print(\"shape:\", tuple(state.shape), \"dtype:\", state.dtype, \"device:\", state.device)\n",
    "\n",
    "    expected = np.array(data) / np.linalg.norm(data)\n",
    "    print(\"GPU amplitudes :\", state[0].real.cpu().numpy().round(4))\n",
    "    print(\"numpy reference:\", expected.round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-28",
   "metadata": {},
   "source": "Batches are where QDP shines. Encode a few thousand samples at once and time it."
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-29",
   "metadata": {},
   "outputs": [],
   "source": [
    "if QDP_OK:\n",
    "    import time\n",
    "\n",
    "    NUM_QUBITS = 10  # 1024 amplitudes per sample\n",
    "    BATCH = 4096\n",
    "    batch = torch.rand(BATCH, 2**NUM_QUBITS, dtype=torch.float32, device=\"cuda\")\n",
    "\n",
    "    torch.cuda.synchronize()\n",
    "    t0 = time.perf_counter()\n",
    "    qtensor = engine.encode(batch, num_qubits=NUM_QUBITS, encoding_method=\"amplitude\")\n",
    "    states = torch.from_dlpack(qtensor)\n",
    "    torch.cuda.synchronize()\n",
    "    elapsed = time.perf_counter() - t0\n",
    "\n",
    "    print(\"encoded:\", tuple(states.shape), states.dtype, states.device)\n",
    "    print(\n",
    "        f\"{BATCH} samples in {elapsed * 1000:.1f} ms  ({BATCH / elapsed:,.0f} samples/s)\"\n",
    "    )\n",
    "\n",
    "    # Each row is a normalized quantum state.\n",
    "    norms = states.abs().pow(2).sum(dim=1)\n",
    "    print(\"row norms (should all be ~1):\", norms[:5].cpu().numpy().round(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-30",
   "metadata": {},
   "source": "Other encoders use the same call: `encoding_method=\"angle\"` (one angle per qubit), `\"basis\"` (an integer index), `\"phase\"`, `\"iqp\"` and `\"iqp-z\"`. The encoded tensor can go straight into a PyTorch model; see `examples/qdp/simple.ipynb` in the repository for a full training loop."
  },
  {
   "cell_type": "markdown",
   "id": "cell-31",
   "metadata": {},
   "source": [
    "## Part 7: Hackathon tasks and how to contribute\n",
    "\n",
    "Today is a **3-hour** hackathon, so every task below is sized for one to two hours, needs no GPU, and ends in a PR with a test. The full sheet with reproduction steps and ready-to-file issue text is `tasks.md` in the hackathon kit; bigger follow-ups are in its appendix.\n",
    "\n",
    "**Track A: Qumat (Python only).** All reproduced on `main` and on `qumat` 0.6.0.\n",
    "\n",
    "| # | Issue | Task |\n",
    "|---|---|---|\n",
    "| A1 | [#1506](https://github.com/apache/mahout/issues/1506) | Clear `ValueError` for an unknown backend name (today a `ModuleNotFoundError` leaks) |\n",
    "| A2 | [#1507](https://github.com/apache/mahout/issues/1507) | Unsupported `simulator_type` should raise instead of `print()` and silently continuing |\n",
    "| A3 | [#1508](https://github.com/apache/mahout/issues/1508) | Cirq backend ignores the initialized simulator and creates its own `cirq.Simulator()` |\n",
    "| A4 | [#1509](https://github.com/apache/mahout/issues/1509) | Bug: reusing a parameter name on two gates fails on Qiskit only (`name conflict adding parameter 't'`) |\n",
    "| A5 | [#1510](https://github.com/apache/mahout/issues/1510) | `create_empty_circuit()` without `num_qubits`: Qiskit raises, Cirq and Braket allocate on demand |\n",
    "| A6 | [#1511](https://github.com/apache/mahout/issues/1511) | Add **one** missing gate per person: S, S-dagger, T-dagger, CZ or Phase, on all three backends with a test |\n",
    "| A7 | [#1512](https://github.com/apache/mahout/issues/1512) | Python 3.13 support: lift the `<3.13` cap in `pyproject.toml` and add 3.13 to CI (all dependencies already ship 3.13 wheels; see Part 0) |\n",
    "\n",
    "**Track B: QDP.** B1 to B3 are pure Python. B4 to B6 are Rust: take one only if `cargo` is already installed, and start the first `cargo build` right away (5 to 10 minutes). No CUDA needed, the build uses stubs like CI does.\n",
    "\n",
    "| # | Issue | Task |\n",
    "|---|---|---|\n",
    "| B1 | [#1469](https://github.com/apache/mahout/issues/1469) | `QdpBenchmark` does not validate builder argument ranges |\n",
    "| B2 | [#1331](https://github.com/apache/mahout/issues/1331) | QDP tests should skip, not error, when no GPU is present |\n",
    "| B3 | [#1468](https://github.com/apache/mahout/issues/1468) | File loader ignores the configured batch limit (one-line wrapper fix plus a test) |\n",
    "| B4 | [#1472](https://github.com/apache/mahout/issues/1472) | Read default LZ4-compressed Feather files (Rust reader) |\n",
    "| B5 | [#787](https://github.com/apache/mahout/issues/787) | NumPy file size check to prevent OOM (Rust reader) |\n",
    "| B6 | [#1436](https://github.com/apache/mahout/issues/1436) | `StreamingProducer`: `VecDeque` for O(1) buffer advance (Rust) |\n",
    "\n",
    "**Setup** (in a terminal, not in Colab; about a minute):\n",
    "\n",
    "```bash\n",
    "git clone https://github.com/<you>/mahout.git && cd mahout\n",
    "pip install uv\n",
    "uv sync                      # Qumat and the Python QDP package\n",
    "uv run pre-commit install    # ruff + type checks on every commit\n",
    "uv run pytest testing/qumat  # smoke test\n",
    "```\n",
    "\n",
    "**Workflow:** comment on the issue to claim it, branch, make the change with a test, open a PR that links the issue. One committer approval is required to merge, so ping a mentor in the room. Discussion happens on dev@mahout.apache.org.\n",
    "\n",
    "Have fun, and thanks for hacking on Apache Mahout!"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "gpuType": "T4",
   "name": "mahout_hackathon_colab.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
