diff --git a/samples/qre/dollar_cost.ipynb b/samples/qre/dollar_cost.ipynb new file mode 100644 index 00000000000..62b8cb7e360 --- /dev/null +++ b/samples/qre/dollar_cost.ipynb @@ -0,0 +1,99 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "89fb2f00", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
qubitsruntimeerrorUSD cost
0321100 days 00:00:00.1225000.003948122.5
\n", + "
" + ], + "text/plain": [ + " qubits runtime error USD cost\n", + "0 32110 0 days 00:00:00.122500 0.003948 122.5" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qdk.estimator import LogicalCounts\n", + "from qdk.qre import estimate\n", + "from qdk.qre.application import QSharpApplication\n", + "from qdk.qre.models import GateBased, RoundBasedFactory, SurfaceCode\n", + "\n", + "app = QSharpApplication(LogicalCounts({\"numQubits\": 100, \"tCount\": 10000, \"cczCount\": 1000, \"measurementCount\": 1000}))\n", + "architecture = GateBased(gate_time=50, measurement_time=1000)\n", + "architecture.usd_cost_per_hour = 3_600.0 * 1000\n", + "\n", + "results = estimate(\n", + " app,\n", + " architecture,\n", + " SurfaceCode.q() * RoundBasedFactory.q(),\n", + " max_error=0.01,\n", + ")\n", + "results.as_frame()" + ] + } + ], + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/source/qdk_package/qdk/qre/_architecture.py b/source/qdk_package/qdk/qre/_architecture.py index 8b98bc7b18e..f598c49d6e3 100644 --- a/source/qdk_package/qdk/qre/_architecture.py +++ b/source/qdk_package/qdk/qre/_architecture.py @@ -40,6 +40,11 @@ class Architecture(ABC): family: TechnologyFamily = TechnologyFamily.UNKNOWN + # Cost of running application, in USD per hour. + # If defined, it's assumed that the cost of running application is proportional to + # the runtime. + usd_cost_per_hour: float | None = None + @abstractmethod def provided_isa(self, ctx: ISAContext) -> ISA: """ @@ -63,6 +68,26 @@ def context(self) -> ISAContext: """ return ISAContext(self) + def cost_usd(self, qubits: int, runtime_nanos: int) -> float | None: + """Estimates cost, in US dollars, of running an application. + + Subclasses need to define `usd_cost_per_hour` (if USD cost is proportional to + runtime) or override `cost_usd`. + + Args: + qubits: estimated number of qubits. + runtime_nanos: estimated runtime, in nanoseconds. + + Returns: + If there is not enough information to estimate cost, returns None. + If application cannot be run on this architecture, returns Infinity. + Otherwise, returns estimated cost of running an application, in dollars. + """ + if self.usd_cost_per_hour is not None: + runtime_hours = runtime_nanos / (3600 * 1e9) + return runtime_hours * self.usd_cost_per_hour + return None + @property def assumptions(self) -> list[str]: """ @@ -233,6 +258,7 @@ def __init__(self, arch: Architecture): self._bindings: dict[str, ISA] = {} self._transforms: dict[int, Architecture | ISATransform] = {0: arch} + self.arch = arch def _with_binding(self, name: str, isa: ISA) -> ISAContext: """Return a new context with an additional binding (internal use).""" diff --git a/source/qdk_package/qdk/qre/_estimation.py b/source/qdk_package/qdk/qre/_estimation.py index ec28847baa4..3611ff2a06f 100644 --- a/source/qdk_package/qdk/qre/_estimation.py +++ b/source/qdk_package/qdk/qre/_estimation.py @@ -300,6 +300,13 @@ def run_estimation(traces: list[Trace]): EstimationTableEntry.from_result(result, arch_ctx) for result in collection ) + if any(e.cost_usd is not None for e in table): + table.add_column( + "USD cost", + lambda entry: entry.cost_usd, + formatter=lambda x: round(x, 2) if x is not None else None, + ) + # Fill in the stats for this estimation run table.stats.num_traces = num_traces table.stats.num_isas = num_isas diff --git a/source/qdk_package/qdk/qre/_results.py b/source/qdk_package/qdk/qre/_results.py index 589a59ef9a1..8eea89bb242 100644 --- a/source/qdk_package/qdk/qre/_results.py +++ b/source/qdk_package/qdk/qre/_results.py @@ -213,6 +213,7 @@ class EstimationTableEntry: and the number of copies required. properties: Additional key-value properties attached to the estimation result. + cost_usd: Cost of running application, in US dollars. """ qubits: int @@ -221,6 +222,7 @@ class EstimationTableEntry: source: InstructionSource factories: dict[int, FactoryResult] = field(default_factory=dict) properties: dict[int, int | float | bool | str] = field(default_factory=dict) + cost_usd: float | None = None @classmethod def from_result( @@ -242,6 +244,7 @@ def from_result( source=InstructionSource.from_isa(ctx, result.isa), factories=result.factories.copy(), properties=result.properties.copy(), + cost_usd=ctx.arch.cost_usd(result.qubits, result.runtime), ) diff --git a/source/qdk_package/tests/qre/test_estimation.py b/source/qdk_package/tests/qre/test_estimation.py index 43e2ad5a4d6..d81a64a5065 100644 --- a/source/qdk_package/tests/qre/test_estimation.py +++ b/source/qdk_package/tests/qre/test_estimation.py @@ -91,6 +91,24 @@ def test_estimation_max_error(): assert next(iter(results)).error <= max_error +def test_estimation_includes_usd_cost(): + app = QSharpApplication(LogicalCounts({"numQubits": 1, "measurementCount": 1})) + arch = GateBased(gate_time=50, measurement_time=100) + arch.usd_cost_per_hour = 2 * 3600 * 1e9 # 2 USD per nanosecond. + + results = estimate( + app, + arch, + SurfaceCode.q() * ExampleFactory.q(), + PSSPC.q() * LatticeSurgery.q(), + ) + assert results[0].runtime == 1050 + + frame = results.as_frame() + assert "USD cost" in frame.columns + assert frame["USD cost"].tolist() == [2100.0] + + @pytest.mark.skipif( "SLOW_TESTS" not in os.environ, reason="turn on slow tests by setting SLOW_TESTS=1 in the environment",