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",
+ " qubits | \n",
+ " runtime | \n",
+ " error | \n",
+ " $ cost | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 18624 | \n",
+ " 0 days 00:00:02.788569 | \n",
+ " 0.009573 | \n",
+ " 474.85 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 24444 | \n",
+ " 0 days 00:00:01.858696 | \n",
+ " 0.008309 | \n",
+ " 316.51 | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 29052 | \n",
+ " 0 days 00:00:01.791981 | \n",
+ " 0.008555 | \n",
+ " 305.15 | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 30264 | \n",
+ " 0 days 00:00:01.394022 | \n",
+ " 0.006875 | \n",
+ " 237.38 | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 38752 | \n",
+ " 0 days 00:00:01.194651 | \n",
+ " 0.008540 | \n",
+ " 271.24 | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " 39964 | \n",
+ " 0 days 00:00:00.929348 | \n",
+ " 0.005442 | \n",
+ " 211.01 | \n",
+ "
\n",
+ " \n",
+ "
\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