-
Notifications
You must be signed in to change notification settings - Fork 1
Gidney 2025 #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gidney 2025 #55
Changes from all commits
0c1e881
8a3be62
48f7885
e223e49
fbe64fe
e053f35
8cf689f
fa6d159
9e33060
a8c3365
37bd6ba
e00932a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
"""Class for a parameterised implementation of Gidney (2025)'s implementation. | ||
|
||
[1] https://arxiv.org/abs/2505.15917 | ||
""" | ||
|
||
from dataclasses import dataclass | ||
from typing import Optional | ||
|
||
from qualtran.resource_counting import GateCounts | ||
from qualtran.surface_code import AlgorithmSummary | ||
|
||
from quantumthreattracker.algorithms.quantum_algorithm import ( | ||
AlgParams, | ||
CryptParams, | ||
QuantumAlgorithm, | ||
) | ||
|
||
|
||
@dataclass | ||
class GidneyBasicParams(AlgParams): | ||
"""(No tunable parameters for this basic implementation; placeholder.).""" | ||
|
||
pass | ||
|
||
|
||
class GidneyBasic(QuantumAlgorithm): | ||
"""Class for a basic implementation of Gidney.""" | ||
|
||
def __init__( | ||
self, | ||
crypt_params: CryptParams, | ||
alg_params: Optional[GidneyBasicParams] = None, | ||
): | ||
"""Initialize the quantum algorithm. | ||
|
||
Parameters | ||
---------- | ||
crypt_params : CryptParams | ||
Cryptographic parameters. | ||
alg_params : Optional[GidneyEkeraBasicParams], optional | ||
Algorithmic parameters. For the basic implementation, these have no effect | ||
but are included for API consistency. | ||
""" | ||
super().__init__(crypt_params, alg_params) | ||
|
||
def get_algorithm_summary( | ||
self, alg_params: Optional[AlgParams] = None | ||
) -> AlgorithmSummary: | ||
"""Compute logical resource estimates for the circuit. | ||
|
||
Parameters | ||
---------- | ||
alg_params : Optional[AlgParams], optional | ||
Algorithm parameters (not used in this basic implementation) | ||
|
||
Returns | ||
------- | ||
AlgorithmSummary | ||
Logical resource estimates. | ||
|
||
Raises | ||
------ | ||
NameError | ||
If the protocol is not "RSA". | ||
ValueError | ||
If the key_size is not supported. | ||
""" | ||
if self._crypt_params.protocol != "RSA": | ||
raise NameError( | ||
'The protocol for this class must be "RSA". ' | ||
+ f'"{self._crypt_params.protocol}" was given.' | ||
) | ||
|
||
key_size = self._crypt_params.key_size | ||
|
||
# Table mapping key_size (n) to Toffolis and Qubits | ||
resource_table = { | ||
1024: {"qubit_count": 742, "toffoli_count": 1.1e9}, | ||
1536: {"qubit_count": 1074, "toffoli_count": 3.1e9}, | ||
2048: {"qubit_count": 1399, "toffoli_count": 6.5e9}, | ||
3072: {"qubit_count": 2043, "toffoli_count": 1.9e10}, | ||
4096: {"qubit_count": 2692, "toffoli_count": 4.0e10}, | ||
6144: {"qubit_count": 3978, "toffoli_count": 1.2e11}, | ||
8192: {"qubit_count": 5261, "toffoli_count": 2.7e11}, | ||
} | ||
|
||
sorted_keys = sorted(resource_table.keys()) | ||
key_size_rounded_up = None | ||
for k in sorted_keys: | ||
if key_size <= k: | ||
key_size_rounded_up = k | ||
break | ||
|
||
if key_size_rounded_up is None: | ||
raise ValueError( | ||
f"Unsupported key_size: {key_size} exceeds maximum supported ({sorted_keys[-1]})" | ||
) | ||
|
||
key_size = key_size_rounded_up | ||
|
||
qubits = int(resource_table[key_size]["qubit_count"]) | ||
toffolis = int(resource_table[key_size]["toffoli_count"]) | ||
|
||
return AlgorithmSummary( | ||
n_algo_qubits=int(qubits), | ||
n_logical_gates=GateCounts(toffoli=toffolis), | ||
) | ||
|
||
@staticmethod | ||
def generate_search_space() -> list[GidneyBasicParams]: | ||
"""Generate a search space for algorithm parameters. | ||
|
||
Since GidneyBasic doesn't have configurable parameters, this returns | ||
a list with a single set of default parameters. | ||
|
||
Returns | ||
------- | ||
list[GidneyBasicParams] | ||
Single-element list containing default parameters. | ||
""" | ||
return [GidneyBasicParams()] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ | |
from datetime import datetime | ||
from pathlib import Path | ||
|
||
import matplotlib.dates as mdates | ||
import matplotlib.pyplot as plt | ||
from matplotlib.axes import Axes | ||
from qsharp.estimator import EstimatorError | ||
|
@@ -225,30 +226,44 @@ def plot_threats(self, protocol: str | None = None) -> Axes: | |
timestamps = [] | ||
runtimes = [] | ||
|
||
ax = plt.subplot(111) | ||
ax.set_yscale("log") | ||
ax.set_xlabel("Timestamp") | ||
ax.set_ylabel("Algorithm runtime (hours)") | ||
ax.xaxis.set_major_locator(mdates.YearLocator()) | ||
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y")) | ||
|
||
if protocol is not None: | ||
report = self.get_report(detail_level=1) | ||
report = [entry for entry in report if entry["protocol"] == protocol] | ||
threats = report[0]["threats"] | ||
|
||
# Remove threats that are dominated by other threats | ||
for threat in threats: | ||
for alt_threat in threats: | ||
# Pareto frontier: keep only threats not dominated by another | ||
# (sooner & shorter runtime) | ||
filtered_threats = [] | ||
for i, t1 in enumerate(threats): | ||
dominated = False | ||
for j, t2 in enumerate(threats): | ||
if j == i: | ||
continue | ||
if ( | ||
threat["timestamp"] >= alt_threat["timestamp"] | ||
and threat["runtime"] >= alt_threat["runtime"] | ||
t2["timestamp"] <= t1["timestamp"] | ||
and t2["runtime"] <= t1["runtime"] | ||
) and ( | ||
t2["timestamp"] < t1["timestamp"] | ||
or t2["runtime"] < t1["runtime"] | ||
): | ||
threats.remove(threat) | ||
dominated = True | ||
break | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The break statement exits the inner loop prematurely when a dominating threat is found, but doesn't prevent checking against remaining threats. This could incorrectly mark a threat as non-dominated when a later threat actually dominates it. Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||
if not dominated: | ||
filtered_threats.append(t1) | ||
threats = filtered_threats | ||
|
||
for entry in threats: | ||
timestamps.append(datetime.fromtimestamp(entry["timestamp"])) | ||
runtimes.append(entry["runtime"] / 3.6e12) | ||
|
||
ax = plt.subplot(111) | ||
ax.plot(timestamps, runtimes, "o--") | ||
ax.set_yscale("log") | ||
ax.set_xlabel("Timestamp") | ||
ax.set_ylabel("Algorithm runtime (hours)") | ||
ax.set_title("Threats against " + protocol) | ||
return ax | ||
|
||
|
@@ -258,13 +273,9 @@ def plot_threats(self, protocol: str | None = None) -> Axes: | |
timestamps.append(datetime.fromtimestamp(entry["threats"][0]["timestamp"])) | ||
runtimes.append(entry["threats"][0]["runtime"] / 3.6e12) | ||
|
||
ax = plt.subplot(111) | ||
ax.scatter(timestamps, runtimes) | ||
for i, txt in enumerate(labels): | ||
ax.annotate(txt, (timestamps[i], runtimes[i])) | ||
ax.set_yscale("log") | ||
ax.set_xlabel("Timestamp") | ||
ax.set_ylabel("Algorithm runtime (hours)") | ||
ax.set_title("Estimates of when cryptographic protocols will be broken") | ||
ax.spines[["right", "top"]].set_visible(False) | ||
return ax |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
"""Tests for the `GidneyBasic` class.""" | ||
|
||
import pytest | ||
|
||
from quantumthreattracker.algorithms.quantum_algorithm import CryptParams | ||
from quantumthreattracker.algorithms.rsa.gidney_basic import ( | ||
GidneyBasic, | ||
GidneyBasicParams, | ||
) | ||
|
||
|
||
def test_gidney_basic_rsa_supported_key_sizes() -> None: | ||
"""Test key size support and algorithm summary generation.""" | ||
key_sizes = [1024, 1536, 2048, 3072, 4096, 6144, 8192] | ||
for key_size in key_sizes: | ||
crypt_params = CryptParams(protocol="RSA", key_size=key_size) | ||
algo = GidneyBasic(crypt_params) | ||
summary = algo.get_algorithm_summary() | ||
assert summary.n_algo_qubits > 0 | ||
assert summary.n_logical_gates.toffoli > 0 | ||
|
||
|
||
def test_gidney_basic_rsa_unsupported_key_size_raises() -> None: | ||
"""Test that GidneyBasic raises ValueError for unsupported RSA key sizes.""" | ||
crypt_params = CryptParams(protocol="RSA", key_size=10000) | ||
algo = GidneyBasic(crypt_params) | ||
with pytest.raises(ValueError): | ||
algo.get_algorithm_summary() | ||
|
||
|
||
def test_gidney_basic_non_rsa_protocol_raises() -> None: | ||
"""Test that GidneyBasic raises NameError when a non-RSA protocol is provided.""" | ||
crypt_params = CryptParams(protocol="ECC", key_size=2048) | ||
algo = GidneyBasic(crypt_params) | ||
with pytest.raises(NameError): | ||
algo.get_algorithm_summary() | ||
|
||
|
||
def test_gidney_basic_generate_search_space() -> None: | ||
"""Test search space generation.""" | ||
params_list = GidneyBasic.generate_search_space() | ||
assert isinstance(params_list, list) | ||
assert len(params_list) == 1 | ||
assert isinstance(params_list[0], GidneyBasicParams) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring references 'GidneyEkeraBasicParams' but should reference 'GidneyBasicParams' to match the actual parameter type.
Copilot uses AI. Check for mistakes.