{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Results and Observables\n", "\n", "*What you will learn:*\n", "\n", "- how to configure an `Observable` to measure quantities of interest in an emulation;\n", "- what observables are available by default;\n", "- how to retrieve the measured observables from a `Results` instance." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `Observable` mechanism\n", "\n", "As showcased in the page on [backend execution](tutorials/backends.nblink#4.-Configuring-the-Emulation), the `Observable` mechanism provides an efficient and uniform way of calculating and storing different quantities of interest throughout an **emulation**. \n", "\n", "It is a shared mechanism between the different emulator backends that can generally be used interchangeably with minimal to no modifications." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Available Observables\n", "\n", "
\n", "\n", "Info\n", "\n", "The observables below are typical quantities of interest that can generally be given to any emulator backend. \n", "\n", "Nonetheless, specific emulators may choose to define additional observables or even modify existing ones, so please make sure to *consult your chosen emulator's specific documentation*. \n", "\n", "
" ] }, { "cell_type": "raw", "metadata": { "editable": true, "raw_mimetype": "text/x-rst", "slideshow": { "slide_type": "" }, "tags": [], "vscode": { "languageId": "raw" } }, "source": [ "The :py:mod:`pulser.backend` module gives access to the following observables by default\n", "\n", ".. currentmodule:: pulser\n", "\n", ".. autosummary::\n", "\n", " pulser.backend.BitStrings\n", " pulser.backend.CorrelationMatrix\n", " pulser.backend.Energy\n", " pulser.backend.EnergySecondMoment\n", " pulser.backend.EnergyVariance\n", " pulser.backend.Expectation\n", " pulser.backend.Fidelity\n", " pulser.backend.Occupation\n", " pulser.backend.StateResult" ] }, { "cell_type": "markdown", "metadata": { "vscode": { "languageId": "raw" } }, "source": [ "### Configuring an `Observable`\n", "\n", "After choosing one or more observables, you must then configure them and **provide them to the** `EmulationConfig` (or the chosen backend's specific `EmulationConfig` subclass).\n", "\n", "To do so,\n", "- Follow the observable's docstring to instantiate it with the required arguments (if any).\n", "- Optionally, you may also specify custom `evaluation_times` for a given observable - when not given, the emulator will simply use `EmulationConfig.default_evaluation_times` instead.\n", "\n", "As a simple example, imagine that by default you only care about an observable's value at the end of the sequence, but you are interested in knowing the energy of the system at the beginning and halfway points of the\n", "execution too. In this case, you could define your observables as follows:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from pulser.backend import BitStrings, Energy, EmulationConfig\n", "\n", "config = EmulationConfig(\n", " default_evaluation_times=[\n", " 1.0\n", " ], # By default, compute an observable only at the end\n", " observables=[\n", " BitStrings(), # No custom evaluation times -> Will be computed only at the end\n", " Energy(\n", " evaluation_times=[0.0, 0.5, 1.0]\n", " ), # Will be computed at the beginning, middle and end\n", " ],\n", ")" ] }, { "cell_type": "markdown", "metadata": { "vscode": { "languageId": "raw" } }, "source": [ "
\n", "\n", "Note\n", "\n", "`evaluation_times` are given as fractions of a sequence's total duration, so `0.0` corresponds to the beginning, `0.5` to the halfway point and `1.0` to the end of the sequence. \n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `State`- or `Operator`-dependent observables\n", "\n", "While most observables can be defined without any arguments, there are two notable exceptions:\n", "\n", "- `Fidelity` requires a `State` instance to know against which state it should compute the fidelity;\n", "- `Expectation` requires an `Operator` instance to know which operator's expectation value to compute.\n", "\n", "Furthermore, both the `State` and `Operator` subclasses used **must be compatible with the chosen backend**. To make sure this is the case, follow these steps:\n", "\n", "1. Pick your target `EmulatorBackend`\n", "2. Take the preferred `EmulationConfig` class from your chosen backend via `EmulatorBackend.config_type`\n", "3. Take the preferred `State` or `Operator` from the config class via `EmulationConfig.state_type` or `EmulationConfig.operator_type`, respectively.\n", "\n", "
\n", "\n", "Tip\n", "\n", "The example shown below is purposefully agnostic of the emulator backend, all it takes to change backend is to change the `emu_backend_class` variable. \n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pulser_simulation\n", "from pulser.backend import Expectation, Fidelity\n", "\n", "# Pick a backend, here we chose QutipBackendV2\n", "emu_backend_class = pulser_simulation.QutipBackendV2\n", "\n", "config_class = emu_backend_class.config_type # In this case, `QutipConfig`\n", "state_class = config_class.state_type # In this case, `QutipState`\n", "operator_class = config_class.operator_type # In this case, `QutipOperator`\n", "\n", "\n", "# Arbitrarily chosen fidelity state |rr>\n", "r_state = state_class.from_state_amplitudes(\n", " eigenstates=(\"r\", \"g\"),\n", " amplitudes={\"rr\": 1.0},\n", ")\n", "# Use `tag_suffix` to better identify the observable in the Results\n", "fidelity = Fidelity(r_state, tag_suffix=\"rr\")\n", "\n", "# Arbitrarily chosen operator XX (where X = |r>\n", "\n", "```python\n", "import numpy as np\n", "\n", "import pulser\n", "import pulser_simulation\n", "from pulser.backend import (\n", " BitStrings,\n", " Energy,\n", " Expectation,\n", " Fidelity,\n", " StateResult,\n", ")\n", "\n", "# STEP 0: Make an arbitrary Pulser Sequence\n", "reg = pulser.Register({\"q0\": (-5, 0), \"q1\": (5, 0)})\n", "\n", "seq = pulser.Sequence(reg, pulser.AnalogDevice)\n", "seq.declare_channel(\"rydberg_global\", \"rydberg_global\")\n", "\n", "t = 2000 # ns\n", "amp_wf = pulser.BlackmanWaveform(duration=t, area=np.pi)\n", "det_wf = pulser.RampWaveform(duration=t, start=-5, stop=5)\n", "seq.add(pulser.Pulse(amp_wf, det_wf, 0), \"rydberg_global\")\n", "\n", "\n", "# STEP 1: Pick the backend and extract the needed classes\n", "emu_backend_class = pulser_simulation.QutipBackendV2\n", "\n", "config_class = emu_backend_class.config_type # In this case, `QutipConfig`\n", "state_class = config_class.state_type # In this case, `QutipState`\n", "operator_class = config_class.operator_type # In this case, `QutipOperator`\n", "\n", "\n", "# STEP 2: Define the desired observables\n", "\n", "# Takes `config.default_num_shots` at the `config.default_evaluation_times`\n", "bitstrings = BitStrings()\n", "\n", "# Records the state of the systems at the `config.default_evaluation_times`\n", "state_obs = StateResult()\n", "\n", "# Records the energy of the system at the beginning, middle and end\n", "energy = Energy(evaluation_times=[0.0, 0.5, 1.0])\n", "\n", "# Records fidelity with |rr> at the `config.default_evaluation_times`\n", "r_state = state_class.from_state_amplitudes(\n", " eigenstates=(\"r\", \"g\"),\n", " amplitudes={\"rr\": 1.0},\n", ")\n", "fidelity = Fidelity(r_state, tag_suffix=\"rr\")\n", "\n", "# Records expectation value of XX at custom evaluation times\n", "pauli_x = operator_class.from_operator_repr(\n", " eigenstates=(\"r\", \"g\"),\n", " n_qudits=2,\n", " operations=[(1.0, [({\"rg\": 1.0, \"gr\": 1.0}, {0, 1})])],\n", ")\n", "expectation = Expectation(\n", " pauli_x, evaluation_times=[0.0, 0.25, 0.5, 0.75, 1.0], tag_suffix=\"XX\"\n", ")\n", "\n", "# STEP 3: Creating a new config with the defined observables\n", "\n", "config = config_class(\n", " observables=[bitstrings, state_obs, energy, fidelity, expectation],\n", " default_evaluation_times=[\n", " 1.0\n", " ], # By default, compute an observable only at the end\n", ")\n", "\n", "# STEP 4: Run the emulation to get the `Results`\n", "\n", "emu_backend = emu_backend_class(seq, config=config)\n", "results = emu_backend.run()\n", "```\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "Info\n", "\n", "The toggled \"Details\" above hides a self-contained script replicating many of the steps outlined in the previous section or in the [backend execution page](tutorials/backends.nblink#1.-Creating-the-Pulse-Sequence). \n", "\n", "Feel free to [skip ahead](#Printing-to-get-an-overview-of-the-Results) if you wish to jump straight into how to access `Results`.\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "editable": true, "nbsphinx": "hidden", "slideshow": { "slide_type": "" }, "tags": [] }, "outputs": [], "source": [ "# NOTE: This should be a hidden cell. If it appears in the built docs,\n", "# make sure `\"nbsphinx\": \"hidden\"` is in the cell's metadata.\n", "\n", "# -----------------------------------------------------------------------------------\n", "# MAKE SURE THE CODE IN THIS CELL IS THE SAME AS IN THE TOGGLED MARKDOWN CELL ABOVE\n", "# -----------------------------------------------------------------------------------\n", "\n", "import numpy as np\n", "\n", "import pulser\n", "import pulser_simulation\n", "from pulser.backend import (\n", " BitStrings,\n", " Energy,\n", " Expectation,\n", " Fidelity,\n", " StateResult,\n", ")\n", "\n", "# STEP 0: Make an arbitrary Pulser Sequence\n", "reg = pulser.Register({\"q0\": (-5, 0), \"q1\": (5, 0)})\n", "\n", "seq = pulser.Sequence(reg, pulser.AnalogDevice)\n", "seq.declare_channel(\"rydberg_global\", \"rydberg_global\")\n", "\n", "t = 2000 # ns\n", "amp_wf = pulser.BlackmanWaveform(duration=t, area=np.pi)\n", "det_wf = pulser.RampWaveform(duration=t, start=-5, stop=5)\n", "seq.add(pulser.Pulse(amp_wf, det_wf, 0), \"rydberg_global\")\n", "\n", "\n", "# STEP 1: Pick the backend and extract the needed classes\n", "emu_backend_class = pulser_simulation.QutipBackendV2\n", "\n", "config_class = emu_backend_class.config_type # In this case, `QutipConfig`\n", "state_class = config_class.state_type # In this case, `QutipState`\n", "operator_class = config_class.operator_type # In this case, `QutipOperator`\n", "\n", "\n", "# STEP 2: Define the desired observables\n", "\n", "# Takes `config.default_num_shots` at the `config.default_evaluation_times`\n", "bitstrings = BitStrings()\n", "\n", "# Records the state of the systems at the `config.default_evaluation_times`\n", "state_obs = StateResult()\n", "\n", "# Records the energy of the system at the beginning, middle and end\n", "energy = Energy(evaluation_times=[0.0, 0.5, 1.0])\n", "\n", "# Records fidelity with |rr> at the `config.default_evaluation_times`\n", "r_state = state_class.from_state_amplitudes(\n", " eigenstates=(\"r\", \"g\"),\n", " amplitudes={\"rr\": 1.0},\n", ")\n", "fidelity = Fidelity(r_state, tag_suffix=\"rr\")\n", "\n", "# Records expectation value of XX at custom evaluation times\n", "pauli_x = operator_class.from_operator_repr(\n", " eigenstates=(\"r\", \"g\"),\n", " n_qudits=2,\n", " operations=[(1.0, [({\"rg\": 1.0, \"gr\": 1.0}, {0, 1})])],\n", ")\n", "expectation = Expectation(\n", " pauli_x, evaluation_times=[0.0, 0.25, 0.5, 0.75, 1.0], tag_suffix=\"XX\"\n", ")\n", "\n", "# STEP 3: Creating a new config with the defined observables\n", "\n", "config = config_class(\n", " observables=[bitstrings, state_obs, energy, fidelity, expectation],\n", " default_evaluation_times=[\n", " 1.0\n", " ], # By default, compute an observable only at the end\n", ")\n", "\n", "# STEP 4: Run the emulation to get the `Results`\n", "\n", "emu_backend = emu_backend_class(seq, config=config)\n", "results = emu_backend.run()\n", "\n", "# -----------------------------------------------------------------------------------\n", "# MAKE SURE THE CODE IN THIS CELL IS THE SAME AS IN THE TOGGLED MARKDOWN CELL ABOVE\n", "# -----------------------------------------------------------------------------------" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Printing to get an overview of the `Results`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Given a `Results` instance, the first thing we can do is print it to get an overview of what it contains" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(results)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, it contains results for each observable we defined, at the requested evaluation times. Now, there are multiple ways we can access these results.\n", "\n", "### Getting a list of results for each observable\n", "\n", "`Results.get_tagged_results()` returns a dictionary with a list of values for each observable, containing one value per evaluation time. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "results.get_tagged_results()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get the list of values for a particular observable, we can either access the dictionary directly or use a shortcut:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# These are equivalent\n", "results.get_tagged_results()[\"fidelity_rr\"]\n", "results.fidelity_rr" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Getting the evaluation times of an observable" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# These are all equivalent\n", "obs = expectation\n", "results.get_result_times(obs)\n", "results.get_result_times(obs.tag)\n", "results.get_result_times(\"expectation_XX\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Getting the result of an observable at a specific evaluation time" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "results.get_result(\"energy\", 0.5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Shortcuts for final bitstrings and state\n", "\n", "Since backend runs typically include the `BitStrings` observable at the end of the sequence, there is a dedicated shortcut to access it." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# These are equivalent\n", "results.get_result(\"bitstrings\", 1.0)\n", "results.final_bitstrings" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The same goes for the quantum state at the end of the emulation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# These are equivalent\n", "results.get_result(\"state\", 1.0)\n", "results.final_state" ] } ], "metadata": { "kernelspec": { "display_name": "__venv__", "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.10.12" } }, "nbformat": 4, "nbformat_minor": 4 }