Logical Error Rate Evaluation

[1]:
# This cell is tagged `parameters` and will be override by `papermill`
# all `=` should be replaced by `@` in the string, otherwise papermill won't be able to parse them
decoder_eval: str = '"mwpm"'
code_eval: str = 'f"rsc(d@{c.d},p@{p})"'
noise_eval: str = '"none"'
config_vec_eval: str = '[dmap(d@3), dmap(d@5), dmap(d@7), dmap(d@9), dmap(d@11)]'

p_center: float = 0.01  # this p value is choosen such that the logical error rate is not too high or too low
per10_p_count: int = 3  # how many p to take per x10 interval
p_bias: int = 0  # the center p will be adjusted to p_center * (10 ** (p_bias / per10_p_count))

slurm_maximum_jobs = 30  # start with a smaller number of workers to avoid resource waste
slurm_cores_per_node: int = 10  # (slurm_maximum_jobs // slurm_cores_per_node) should not exceed 200
slurm_mem_per_job: int = 2  # 2GB per job
slurm_extra = dict(
    walltime = "1-00:00:00",  # adaptively shutdown if no more jobs
    queue = "scavenge",  # use with caution: dask does not seem to handle scavenge workers well
    job_extra_directives = ["--requeue"],  # use with scavenge partition will help spawn scavenged jobs
)

import multiprocessing
local_maximum_jobs = multiprocessing.cpu_count()
# local_maximum_jobs = 10

json_filename: str | None = None
force_finished: bool = False  # only plot the figure and do not run experiments
[2]:
# to get around papermill limitation: there cannot be = character in the string
decoder_eval = decoder_eval.replace("@", "=")
code_eval = code_eval.replace("@", "=")
noise_eval = noise_eval.replace("@", "=")
config_vec_eval = config_vec_eval.replace("@", "=")
[ ]:
from slugify import slugify
from dotmap import DotMap as dmap

config_vec = eval(config_vec_eval)
print(config_vec)

if json_filename is None:
    json_filename = "zdat-" + slugify(code_eval) + "." + slugify(noise_eval) + "." + slugify(decoder_eval) + ".json"
print(json_filename)
[4]:
%load_ext autoreload
%autoreload 2
[5]:
from qec_lego_bench.hpc.monte_carlo import *
from qec_lego_bench.hpc.submitter import *
from qec_lego_bench.hpc.plotter import *
from typing import Iterable
from qec_lego_bench.cli.logical_error_rate import logical_error_rate

Define the job list

[ ]:
ap_vec = AdaptivePVec(p_center=p_center, per10_p_count=per10_p_count, p_bias=p_bias)
jobs = [MonteCarloJob(config=config, p=ap_vec.biased_p_center) for config in config_vec]


def monte_carlo_function(
    shots: int, config: dmap, p: float
) -> tuple[int, LogicalErrorResult]:
    c = config
    decoder = eval(decoder_eval)
    code = eval(code_eval)
    noise = eval(noise_eval)
    stats = logical_error_rate(
        decoder=decoder,
        code=code,
        noise=noise,
        max_shots=shots,
        max_errors=shots,
        no_progress=True,
        no_print=True,
    )
    return stats.shots, LogicalErrorResult.from_stats(stats)

print(ap_vec.biased_p_center, monte_carlo_function(1000, config_vec[0], ap_vec.biased_p_center))

Define the strategy to submit jobs

[7]:
adaptive_submitter = AdaptivePVecSubmitter(
    config_vec=config_vec,
    ap_vec=ap_vec,
    time_limit=3600,
    min_shots=1000,
    target_precision=0.1,
)
precision_submitter = PrecisionSubmitter(
    time_limit=100 * 3600, min_precision=1, target_precision=0.03
)


def submitter(executor: MonteCarloJobExecutor) -> list[tuple[MonteCarloJob, int]]:
    submit = adaptive_submitter(executor)
    if len(submit) == 0 and executor.no_pending():  # previous submitter all finished
        submit += precision_submitter(executor)
    return submit

The rest of the notebook runs the evaluation

Start a cluster by intelligently choose Slurm or Local cluster.

[ ]:
client_connector = SlurmClientConnector(
    slurm_maximum_jobs=slurm_maximum_jobs,
    slurm_cores_per_node=slurm_cores_per_node,
    slurm_mem_per_job=slurm_mem_per_job,
    slurm_extra=slurm_extra,
)
[9]:
config = MonteCarloExecutorConfig()
config.max_submitted_job = max(config.max_submitted_job, 3 * slurm_maximum_jobs)
executor = MonteCarloJobExecutor(
    monte_carlo_function,
    jobs,
    config=config,
    filename=json_filename,
)

Define the callback, e.g. plotting the intermediate result and the list of remaining tasks

(I have to put them in the same block as the actual execution, otherwise it won’t update in VScode)

[ ]:
import time  # add some sleep to let them work properly in VScode Jupyter notebook

time.sleep(0.2)
plotter = AdaptivePVecPlotter(config_vec=config_vec, ap_vec=ap_vec)
time.sleep(0.2)
progress_plotter = JobProgressPlotter()
time.sleep(0.2)
memory_plotter = MemoryUsagePlotter()
time.sleep(0.2)


def callback(executor: MonteCarloJobExecutor):
    plotter(executor)
    time.sleep(0.1)
    progress_plotter(executor)
    time.sleep(0.1)
    memory_plotter(executor)
    time.sleep(0.1)

print(
    "will shut down the cluster after job finishes; if this is not desired, set `shutdown_cluster` to False"
)

executor.execute(
    client_connector=client_connector,
    submitter=submitter,
    loop_callback=callback,
    shutdown_cluster=True,
    force_finished=force_finished,
)
[ ]: