diff --git a/samples/qre/cost_example.ipynb b/samples/qre/cost_example.ipynb new file mode 100644 index 00000000000..51ba5ae0806 --- /dev/null +++ b/samples/qre/cost_example.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4d47cb98-6af6-4757-b450-c2209ce8f5df", + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
qubitsruntimeerror$ cost
0186240 days 00:00:02.7885690.009573474.85
1244440 days 00:00:01.8586960.008309316.51
2290520 days 00:00:01.7919810.008555305.15
3302640 days 00:00:01.3940220.006875237.38
4387520 days 00:00:01.1946510.008540271.24
5399640 days 00:00:00.9293480.005442211.01
\n", + "
" + ], + "text/plain": [ + " qubits runtime error $ cost\n", + "0 18624 0 days 00:00:02.788569 0.009573 474.85\n", + "1 24444 0 days 00:00:01.858696 0.008309 316.51\n", + "2 29052 0 days 00:00:01.791981 0.008555 305.15\n", + "3 30264 0 days 00:00:01.394022 0.006875 237.38\n", + "4 38752 0 days 00:00:01.194651 0.008540 271.24\n", + "5 39964 0 days 00:00:00.929348 0.005442 211.01" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "import qdk\n", + "from qdk import qsharp\n", + "from qdk.qre import estimate, property_name, instruction_name, PSSPC, LatticeSurgery\n", + "from qdk.qre.models import GateBased, SurfaceCode, RoundBasedFactory\n", + "from qdk.qre.application import QSharpApplication\n", + "\n", + "from qdk.qre.instruction_ids import T, LATTICE_SURGERY\n", + "from qdk.qre.property_keys import NUM_TS_PER_ROTATION\n", + "\n", + "# Evaluate the Q# application into the scope\n", + "qsharp.eval(Path(\"../algorithms/Shor.qs\").read_text(encoding=\"utf-8\"))\n", + "\n", + "# The entry point in the Q# code above is called Main\n", + "app = QSharpApplication(qdk.code.Main)\n", + "\n", + "arch = GateBased(error_rate=1e-4, gate_time=100, measurement_time=500)\n", + "\n", + "results = estimate(\n", + " app,\n", + " arch,\n", + " isa_query=SurfaceCode.q() * RoundBasedFactory.q(),\n", + " trace_query=PSSPC.q() * LatticeSurgery.q(slow_down_factor=[1.0, 1.5, 2.0, 3.0, 4.0]),\n", + " max_error=0.01,\n", + " cost_spec_path=\"example_cost_spec.json\"\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/samples/qre/example_cost_spec.json b/samples/qre/example_cost_spec.json new file mode 100644 index 00000000000..c312a03af19 --- /dev/null +++ b/samples/qre/example_cost_spec.json @@ -0,0 +1,43 @@ +{ + "system": { + "name": "example_cost_spec", + "qubits_per_node": 16000, + "cnot_time": 10000, + "cnot_error_rate": 0.001, + "one_qubit_time": 1000, + "one_qubit_error_rate": 0.0001, + "measurement_time": 1000, + "measurement_error_rate": 0.0001, + "target_year": 2030, + "lifetime_in_years": 10, + "uptime": 0.9, + "control_lines_per_physical_qubit": 1, + "readout_lines_per_physical_qubit": 0, + "fixed_control_lines": 0, + "fixed_readout_lines": 0, + "magical_speedup_factor": 1 + }, + "fixed_units": [ + { + "name": "Fixed cost of constructing the system", + "cost": 10000000.0, + "quantity": 1, + "eos_factor": 1.0 + } + ], + "scaled_units": [ + { + "name": "Black box cost per qubit", + "cost": 1000000.00, + "units_per_control_line": 1, + "units_per_readout_line": 0, + "eos_factor": 1.0 + } + ], + "opex": [ + { + "name": "FTE costs", + "cost": 10000000 + } + ] +} \ No newline at end of file diff --git a/source/qdk_package/qdk/qre/_dollar_cost.py b/source/qdk_package/qdk/qre/_dollar_cost.py new file mode 100644 index 00000000000..fe8ec67ee51 --- /dev/null +++ b/source/qdk_package/qdk/qre/_dollar_cost.py @@ -0,0 +1,48 @@ +import math + +from ._json_specs import JsonSpec +from ._results import EstimationTableEntry + + +def compute_dollar_cost(cost_spec_path: str, result: EstimationTableEntry) -> float: + """Computes cost in dollars.""" + spec = JsonSpec.from_file(cost_spec_path) + system = spec.system + + # Build the node cost + qubits_per_node = system.qubits_per_node + control_lines = ( + system.fixed_control_lines + + qubits_per_node * system.control_lines_per_physical_qubit + ) + readout_lines = ( + system.fixed_readout_lines + + qubits_per_node * system.readout_lines_per_physical_qubit + ) + cost_per_control_line = 0 + for unit in spec.scaled_units: + cost_per_control_line += ( + unit.cost * unit.units_per_control_line * unit.eos_factor + ) + cost_per_readout_line = 0 + for unit in spec.scaled_units: + cost_per_readout_line += ( + unit.cost * unit.units_per_readout_line * unit.eos_factor + ) + + capex = 0 + for unit in spec.fixed_units: + capex += unit.cost * unit.quantity * unit.eos_factor + capex += cost_per_control_line * control_lines + capex += cost_per_readout_line * readout_lines + + opex = sum(item.cost for item in spec.opex) + + number_of_nodes = math.ceil(result.qubits / qubits_per_node) + 1 + runtime_hours = result.runtime / (3600 * 1_000_000_000) # Convert to hours + runtime_hours /= system.magical_speedup_factor + operating_lifetime = system.lifetime_in_years + up_hours_per_year = 8760 * system.uptime + hourly_cost = (opex + capex / operating_lifetime) / up_hours_per_year + dollar_cost = number_of_nodes * hourly_cost * runtime_hours + return math.ceil(dollar_cost * 100) / 100 diff --git a/source/qdk_package/qdk/qre/_estimation.py b/source/qdk_package/qdk/qre/_estimation.py index 6191ac4b455..b7af505fea3 100644 --- a/source/qdk_package/qdk/qre/_estimation.py +++ b/source/qdk_package/qdk/qre/_estimation.py @@ -3,34 +3,36 @@ from __future__ import annotations -from typing import cast, Optional, Any +from typing import Any, cast from .. import telemetry_events from ._application import Application from ._architecture import Architecture +from ._dollar_cost import compute_dollar_cost +from ._isa_enumeration import ISAQuery from ._qre import ( - _estimate_parallel, - _estimate_with_graph, - _EstimationCollection, ErrorComposition, Trace, + _EstimationCollection, + _estimate_parallel, + _estimate_with_graph, ) -from ._trace import TraceQuery, PSSPC, LatticeSurgery -from ._isa_enumeration import ISAQuery from ._results import EstimationTable, EstimationTableEntry +from ._trace import LatticeSurgery, PSSPC, TraceQuery def estimate( application: Application, architecture: Architecture, isa_query: ISAQuery, - trace_query: Optional[TraceQuery] = None, + trace_query: TraceQuery | None = None, *, max_error: float = 1.0, post_process: bool = False, use_graph: bool = True, composition: ErrorComposition = ErrorComposition.UnionBound, - name: Optional[str] = None, + name: str | None = None, + cost_spec_path: str | None = None, ) -> EstimationTable: """ Estimate the resource requirements for a given application instance and @@ -82,6 +84,8 @@ def estimate( ``1 - prod(1 - p_i)``. name (Optional[str]): An optional name for the estimation. If given, this will be added as a first column to the results table for all entries. + json_spec_path (Optional[str]): Path to the system specification in + JSON format. If provided, a ``$ cost`` column is added to the results. Returns: EstimationTable: A table containing the optimal estimation results. @@ -223,6 +227,12 @@ def estimate( EstimationTableEntry.from_result(result, arch_ctx) for result in collection ) + if cost_spec_path is not None: + table.add_column( + "$ cost", + lambda entry: compute_dollar_cost(cost_spec_path, entry), + ) + # 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/_json_specs.py b/source/qdk_package/qdk/qre/_json_specs.py new file mode 100644 index 00000000000..14944d921c0 --- /dev/null +++ b/source/qdk_package/qdk/qre/_json_specs.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import json +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any + + +@dataclass(frozen=True) +class System: + """Hardware and operating assumptions for the quantum system. + + Attributes: + name: Human-readable specification name. + qubits_per_node: Number of physical qubits available in one node. + cnot_time: CNOT gate duration in nanoseconds. + cnot_error_rate: Error probability for a CNOT gate. + one_qubit_time: Single-qubit gate duration in nanoseconds. + one_qubit_error_rate: Error probability for a single-qubit gate. + measurement_time: Measurement duration in nanoseconds. + measurement_error_rate: Error probability for a measurement. + target_year: Calendar year targeted by the specification. + lifetime_in_years: Expected operating lifetime of the system. + uptime: Fraction of each year during which the system is available. + control_lines_per_physical_qubit: Control lines required per qubit. + readout_lines_per_physical_qubit: Readout lines required per qubit. + fixed_control_lines: Control lines required independently of qubit count. + fixed_readout_lines: Readout lines required independently of qubit count. + magical_speedup_factor: Runtime reduction supplied by external assumptions. + """ + + name: str + qubits_per_node: int + cnot_time: int + cnot_error_rate: float + one_qubit_time: int + one_qubit_error_rate: float + measurement_time: int + measurement_error_rate: float + target_year: int + lifetime_in_years: float + uptime: float + control_lines_per_physical_qubit: float + readout_lines_per_physical_qubit: float + fixed_control_lines: int + fixed_readout_lines: int + magical_speedup_factor: float + + +@dataclass(frozen=True) +class FixedUnit: + """A system component whose quantity does not scale with line count. + + Attributes: + name: Human-readable component name. + cost: Cost of one component in dollars. + quantity: Number of components required. + eos_factor: End-of-support multiplier applied to the component cost. + """ + + name: str + cost: float + quantity: float + eos_factor: float + + +@dataclass(frozen=True) +class ScaledUnit: + """A component whose quantity scales with control or readout lines. + + Attributes: + name: Human-readable component name. + cost: Cost of one component in dollars. + eos_factor: End-of-support multiplier applied to the component cost. + units_per_control_line: Components required for each control line. + units_per_readout_line: Components required for each readout line. + """ + + name: str + cost: float + eos_factor: float = 1.0 + units_per_control_line: int = 0 + units_per_readout_line: int = 0 + + +@dataclass(frozen=True) +class OpExUnit: + """A recurring annual operating expense. + + Attributes: + name: Human-readable expense name. + cost: Annual cost in dollars. + """ + + name: str + cost: float + + +@dataclass(frozen=True) +class JsonSpec: + """Typed representation of a complete system cost specification. + + Attributes: + system: Hardware and operating assumptions. + fixed_units: Components whose quantities are fixed per node. + scaled_units: Components whose quantities scale with system lines. + opex: Recurring annual operating expenses. + """ + + system: System + fixed_units: list[FixedUnit] + scaled_units: list[ScaledUnit] + opex: list[OpExUnit] + + # TODO: use dataclasses-json instead. + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> JsonSpec: + """Create a specification from decoded JSON data.""" + return cls( + system=System(**data["system"]), + fixed_units=[FixedUnit(**item) for item in data["fixed_units"]], + scaled_units=[ScaledUnit(**item) for item in data["scaled_units"]], + opex=[OpExUnit(**item) for item in data["opex"]], + ) + + @classmethod + def from_file(cls, path: str) -> JsonSpec: + """Load a specification from a JSON file.""" + with open(path, encoding="utf-8") as file: + return cls.from_dict(json.load(file)) diff --git a/source/qdk_package/tests/qre/test_dollar_cost.py b/source/qdk_package/tests/qre/test_dollar_cost.py new file mode 100644 index 00000000000..42c172ba4d4 --- /dev/null +++ b/source/qdk_package/tests/qre/test_dollar_cost.py @@ -0,0 +1,35 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from pathlib import Path + +from qdk.qre._dollar_cost import compute_dollar_cost +from qdk.qre._instruction import InstructionSource +from qdk.qre._json_specs import JsonSpec, OpExUnit, ScaledUnit, System +from qdk.qre._results import EstimationTableEntry + + +COST_SPEC_PATH = Path(__file__).resolve().parents[4] / "samples/qre/example_cost_spec.json" + + +def test_json_spec_from_file(): + spec = JsonSpec.from_file(str(COST_SPEC_PATH)) + + assert isinstance(spec.system, System) + assert spec.system.name == "example_cost_spec" + assert spec.system.qubits_per_node == 16_000 + assert isinstance(spec.scaled_units[0], ScaledUnit) + assert spec.scaled_units[0].units_per_control_line == 1 + assert isinstance(spec.opex[0], OpExUnit) + assert spec.opex[0].cost == 10_000_000 + + +def test_compute_dollar_cost_from_json_spec(): + entry = EstimationTableEntry( + qubits=16_000, + runtime=3_600_000_000_000, + error=0.0, + source=InstructionSource(), + ) + + assert compute_dollar_cost(str(COST_SPEC_PATH), entry) == 408_675.80