{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Walkthrough 5: Experimentation and batch jobs #\n",
    " "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Introduction ##"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "While results from a single job of the Oqtant hardware is often interesting, commonly multiple jobs are needed to either average results or to scan control parameters to map out dependencies and trends.  In this tutorial, we will explore ways of submitting and managing lists of QuantumMatter objects and resulting OqtantJob(s).  "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Imports and creation of an OqtantClient instance ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from oqtant.schemas.quantum_matter import QuantumMatter\n",
    "from oqtant.oqtant_client import get_oqtant_client\n",
    "from oqtant.util.auth import notebook_login\n",
    "\n",
    "oqtant_account = notebook_login()\n",
    "oqtant_account"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "client = get_oqtant_client(oqtant_account.access_token)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Submitting a list of QuantumMatter objects to generate many independent jobs ##"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "While it is possible to construct QuantumMatter objects one at a time and submit them to Oqtant QMS (via the OqtantClient) individually, one can also submit many objects at once.  Here, we will demonstrate how to create such a list, each element with individually tuned inputs, and submit this list to generate many jobs in one go."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating a list of QuantumMatter objects ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "  Let us begin by making a list of QuantumMatter objects that each have a different target temperature. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "N = 4\n",
    "matters = [\n",
    "    QuantumMatter(temperature=50 * (n + 1), name=\"run\" + str(n + 1) + \"/\" + str(N))\n",
    "    for n in range(N)\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Submitting the list to the client ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can then submit the list to the Oqtant client.  In this case, the *Client.submit_list()* method generates many independent jobs to be run on the Oqtant platform and returns a list of job ids.  Each of these jobs enters Oqtant's queueing system at nearly the same time, so they will *likely* be executed near each other in time, depending on current queue usage."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "my_job_ids = client.submit_list(matter_list=matters, track=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that all 4 jobs receieved a unique job id."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Accessing job results ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once all our submitted jobs are complete, we access the results in exactly the same way we would do had we submitted the programs individually:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# retrieve jobs from server one at a time, creating corresponding local job objects\n",
    "my_jobs = [client.get_job(id) for id in my_job_ids]\n",
    "\n",
    "# and access the results individually\n",
    "for job in my_jobs:\n",
    "    print(job.output.temperature_nk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Submitting a list of QuantumMatter objects to generate a single \"batch\" job ##"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "There is also the option to submit a list of QuantumMatter objects as a single *batch* job, which guarantees that the sequence executes sequentially on the hardware.  This feature is useful for detailed experimentation or investigation, where subsequent shots need to be compared to each other in detail.  Using sequential hardware shots reduces system drift or inconsistency.  In the case of bundling into a single batch job, only one job id will be generated.  Programmatically, the batch job will be composed of multiple *run*s on Oqtant's hardware, and retrieving job results (see below) will require specifying the run number $1 \\ldots N$ when fetching the job results, where there were $N$ runs in the job.    \n",
    "\n",
    "NOTE: The resulting name of a batch job will default to the name given to the first QuantumMatter object in the provided list.  Alternatively, a global name can be provided at the point of submission to the client.  Also be aware that for a batch job each individual *run* is charged against your job quota."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating a list of QuantumMatter objects ###"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# create a list of\n",
    "N = 4\n",
    "matters = [\n",
    "    QuantumMatter(temperature=50 * (n + 1), name=str(n + 1) + \"/\" + str(N))\n",
    "    for n in range(N)\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Submitting the list to the client as a batch job ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now submit our list of QuantumMatter objects to generate a batch job using the OqtantClient.submit_list_as_batch() method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# submit the list as a batch that will run sequentially on the hardware\n",
    "# returns a single job id, e.g., \"1cdb4ff7-c5ed-46d3-a667-8b12f0cd1349\"\n",
    "my_batch_job_id = client.submit_list_as_batch(\n",
    "    matter_list=matters,\n",
    "    name=\"rename as batch\",  # optional\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see that only a single job is generated."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Accessing job results ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can now retrieve the batch job run results one at a time by specifying the desired run number in the OqtantClient.get_job() method.  If omitted (as for non-batch jobs with just a single run), the data for the first run will be fetched.  \n",
    "\n",
    "NOTE: The added complication of managing multiple runs within a single job is is an unavoidable consequence of ensuring that the job runs execute sequentially on the hardware."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# create local jobs based on single runs of a QMS batch job\n",
    "my_batch_job_id = \"1cdb4ff7-c5ed-46d3-a667-8b12f0cd1349\"\n",
    "my_batch_job_first_run = client.get_job(job_id=my_batch_job_id, run=1)\n",
    "my_batch_job_second_run = client.get_job(job_id=my_batch_job_id, run=2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Despite having just created two local jobs based on two sequential runs of our batch job, we can see that both jobs share the same job id, but will in general have different (inputs and) outputs:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(my_batch_job_first_run.id)\n",
    "print(my_batch_job_first_run.output.temperature_nk)\n",
    "\n",
    "print(my_batch_job_second_run.id)\n",
    "print(my_batch_job_second_run.output.temperature_nk)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Saving batch job results ###"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we fetch results from a batch job, our instantiated local job only contains output data for the specified run.  However, fetching multiple runs will result in many local jobs that share the same job id.  When saving the data associated with these jobs, the job id (which is used as the default filename) is therefore not unique.  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "client.write_job_to_file(my_batch_job_first_run)\n",
    "client.write_job_to_file(my_batch_job_second_run)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this case, the `OqtantClient.write_job_to_file()` method automatically appends the run information in the format *id_run_n_of_N.txt*."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
