microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.25.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/src/generic_estimator/factory.rs

439lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::{borrow::Cow, marker::PhantomData};
5
6use pyo3::{
7 Bound, PyAny, PyResult,
8 exceptions::PyLookupError,
9 types::{PyAnyMethods, PyDict, PyList},
10};
11use resource_estimator::estimates::{
12 DistillationRound, Factory, FactoryBuilder, PhysicalQubitCalculation, RoundBasedFactory,
13};
14use round_based::{OrderedBFSControl, ordered_bfs};
15use serde::Serialize;
16use serde_json::{Map, Value, json};
17
18use crate::generic_estimator::utils::maybe_extract_and_check_method;
19
20use super::{
21 code::PythonQEC,
22 utils::{SerializableBound, python_dict_to_json_map},
23};
24
25mod dispatch;
26pub use dispatch::PythonFactoryBuilderDispatch;
27pub(crate) mod round_based;
28pub use round_based::PythonDistillationUnit;
29
30enum FactoryImplementation<'py> {
31 Generic(Bound<'py, PyAny>),
32 RoundBased {
33 distillation_units: Bound<'py, PyAny>,
34 trivial_distillation_unit: Option<Bound<'py, PyAny>>,
35 },
36}
37
38/// A wrapper around a Python instance to compute magic state factories.
39///
40/// There are two ways to model magic state factories: 1) create factories
41/// explicitly, which are modeled by means of their size, runtime, and number of
42/// output states, or 2) return distillation units which can then be composed in
43/// multiple rounds to create a factory.
44///
45/// ## Explicitly creating factories
46///
47/// To create magic state factories explicitly, the class must implement the
48/// function `find_factories`, which takes as input the code that is used for
49/// algorithm qubits (it must not be used by the factory builder, and other
50/// codes could be constructed), the qubit parameters specified by the user, and
51/// the target error rate required for magic states in the current estimation.
52/// The function either returns `None`, if, e.g., no factories can be found that
53/// satisfy the target error rate, or it returns a list of factory objects.
54///
55/// ```python
56/// def find_factories(self, code, qubit, target_error_rate):
57/// # e.g., return [
58/// # {
59/// # "physical_qubits": 42,
60/// # "duration": 655321, # in ns
61/// # "num_output_states": 1
62/// # }
63/// # ]
64/// ...
65/// ```
66///
67/// A factory object is simply a Python dictionary, which _must_ contain
68/// entries for `"physical_qubits"` and `"duration"` (in nano seconds). It may
69/// also contain `"num_output_states"`, which defaults to 1 if not specified. It
70/// may contain other entries, which will be included in the serialized resource
71/// estimates.
72///
73/// ## Creating factories based on multiple rounds of distillation
74///
75/// To create factories based on multiple rounds of distillation, the class must
76/// implement the function `distillation_units`, which returns a list of
77/// distillation unit objects (described below). Further, such a class may
78/// provide local variables `gate_error`, `max_rounds` and `max_extra_rounds`
79/// (by default set to `"gate_error"`, 3 and 5, respectively). The variable
80/// `gate_error` is a string that is used to index the `qubit` parameter for the
81/// magic gate error rate. The variable `max_rounds` controls how many rounds
82/// of distillation should always be explored (even, if a factory is found with
83/// fewer than `max_rounds` rounds). And the variable `max_extra_rounds`
84/// controls by how many extra rounds the search should be extended, if no
85/// factory can be found within `max_rounds` rounds.
86///
87/// ```python
88/// def distillation_units(self, code, qubit, max_code_parameter):
89/// # e.g., return [
90/// # {
91/// # "name": "unit",
92/// # "code_parameter": 5, # must be same type as max_code_parameter
93/// # "num_input_states": 42,
94/// # "num_output_states": 2,
95/// # "physical_qubits": lambda round: 100, # round is 0-based index to when unit is used in factory
96/// # "duration": lambda round: 30, # duration in ns, and round is as above
97/// # "output_error_rate": lambda input_error_rate: ..., # some formula to compute output_error_rate
98/// # "failure_probability": lambda input_error_rate: ... # some formula to compute failure_probability
99/// # }
100/// # ]
101/// ```
102///
103/// A distillation unit object is simply a Python dictionary, which _must_
104/// contain entries for `"num_input_states"`, `"physical_qubits"`, `"duration"`,
105/// `"output_error_rate"`, and `"failure_probability"`. The fields `"name"`,
106/// `"code_parameter"`, and `"num_output_states"` are optional and default to
107/// `"distillation-unit"`, `None`, and `1`. The fields for `"physical_qubits"`
108/// and `"runtime"` must be callable (e.g., using a Python lambda) with the
109/// 0-based round index as only parameter. The fields for `"output_error_rate"`
110/// and `"failure_probability"` also must be callable with the input error rate
111/// (of the previous round or initial gate error rate) as the only parameter.
112pub struct PythonFactoryBuilder<'py> {
113 builder: Bound<'py, PyAny>,
114 implementation: FactoryImplementation<'py>,
115 num_magic_state_types: usize,
116}
117
118impl<'py> PythonFactoryBuilder<'py> {
119 pub fn from_bound(builder: Bound<'py, PyAny>) -> PyResult<Self> {
120 let implementation = if let Ok(find_factories) = builder.getattr("find_factories") {
121 FactoryImplementation::Generic(find_factories)
122 } else if let Ok(distillation_units) = builder.getattr("distillation_units") {
123 let trivial_distillation_unit =
124 maybe_extract_and_check_method(&builder, "trivial_distillation_unit")?;
125 FactoryImplementation::RoundBased {
126 distillation_units,
127 trivial_distillation_unit,
128 }
129 } else {
130 return Err(PyLookupError::new_err(
131 "FactoryBuilder must have either find_factories or distillation_units method",
132 ));
133 };
134
135 let num_magic_state_types = if let Ok(method) = builder.getattr("num_magic_state_types") {
136 method.call0()?.extract()?
137 } else {
138 1
139 };
140
141 Ok(Self {
142 builder,
143 implementation,
144 num_magic_state_types,
145 })
146 }
147
148 fn create_generic_factories<'a>(
149 code: &PythonQEC<'py>,
150 qubit: &Bound<'py, PyDict>,
151 output_error_rate: f64,
152 find_factories: &Bound<'py, PyAny>,
153 ) -> Result<Vec<std::borrow::Cow<'a, PythonFactory<'py>>>, String> {
154 let result = find_factories
155 .call1((
156 code.bound(),
157 std::convert::AsRef::<pyo3::Py<PyAny>>::as_ref(qubit),
158 output_error_rate,
159 ))
160 .map_err(|e| e.to_string())?;
161
162 if result.is_none() {
163 Ok(vec![])
164 } else {
165 let factories = result.cast::<PyList>().map_err(|e| e.to_string())?;
166 let mut converted = vec![];
167
168 for element in factories {
169 let dict = element.cast::<PyDict>().map_err(|e| e.to_string())?;
170 let factory = PythonFactory::from_py_dict(dict).ok_or(format!(
171 "Failed to convert factory from Python dict: {dict:?}, does the dictionary contain entries for 'physical_qubits' and 'duration'?",
172 ))?;
173 converted.push(Cow::Owned(factory));
174 }
175
176 Ok(converted)
177 }
178 }
179
180 fn create_round_based_factories(
181 &self,
182 code: &PythonQEC<'py>,
183 qubit: &Bound<'py, PyDict>,
184 output_error_rate: f64,
185 max_code_parameter: &SerializableBound<'py>,
186 distillation_units: &Bound<'py, PyAny>,
187 trivial_distillation_unit: Option<&Bound<'py, PyAny>>,
188 ) -> Result<Vec<std::borrow::Cow<'_, PythonFactory<'py>>>, String> {
189 // input error rate
190 let qubit_key = self
191 .builder
192 .getattr("gate_error")
193 .and_then(|a| a.extract())
194 .unwrap_or(String::from("gate_error"));
195 let initial_input_error_rate = qubit
196 .get_item(&qubit_key)
197 .map_err(|e| e.to_string())?
198 .extract()
199 .map_err(|e: pyo3::PyErr| e.to_string())?;
200
201 if initial_input_error_rate <= output_error_rate
202 && let Some(trivial_distillation_unit) = trivial_distillation_unit
203 {
204 return Self::create_trivial_distillation_unit(
205 code,
206 qubit,
207 max_code_parameter,
208 trivial_distillation_unit,
209 initial_input_error_rate,
210 );
211 }
212
213 let use_max_qubits_per_round = self
214 .builder
215 .getattr("use_max_qubits_per_round")
216 .and_then(|a| a.extract())
217 .unwrap_or(false);
218 let max_rounds = self
219 .builder
220 .getattr("max_rounds")
221 .and_then(|a| a.extract())
222 .unwrap_or(3);
223 let max_extra_rounds = self
224 .builder
225 .getattr("max_extra_rounds")
226 .and_then(|a| a.extract())
227 .unwrap_or(5);
228
229 let return_value = distillation_units
230 .call1((
231 code.bound(),
232 std::convert::AsRef::<pyo3::Py<PyAny>>::as_ref(qubit),
233 &**max_code_parameter,
234 ))
235 .map_err(|e| e.to_string())?;
236
237 let units: Vec<_> = return_value
238 .try_iter()
239 .map_err(|e| {
240 format!("{e} (check the return value of the 'distillation_units' method)",)
241 })?
242 .map(|bound| PythonDistillationUnit::new(bound?.cast_into::<PyDict>()?))
243 .collect::<Result<_, _>>()
244 .map_err(|e| e.to_string())?;
245
246 let mut factories: Vec<Cow<PythonFactory>> = vec![];
247
248 ordered_bfs(&units, max_extra_rounds, |selected_units| {
249 if selected_units.len() > max_rounds && !factories.is_empty() {
250 return Ok(OrderedBFSControl::Terminate);
251 }
252
253 if let Ok(mut factory) =
254 RoundBasedFactory::build(&selected_units, initial_input_error_rate, 0.01)
255 && factory.output_error_rate() <= output_error_rate
256 {
257 factory.set_physical_qubit_calculation(if use_max_qubits_per_round {
258 PhysicalQubitCalculation::Max
259 } else {
260 PhysicalQubitCalculation::Sum
261 });
262 factories.push(Cow::Owned(PythonFactory::try_from(factory)?));
263
264 if selected_units.len() > max_rounds {
265 return Ok(OrderedBFSControl::Terminate);
266 }
267 }
268
269 Ok(OrderedBFSControl::Continue)
270 })?;
271
272 if factories.is_empty() {
273 return Err(format!(
274 "No factories found for output error rate: {output_error_rate}, try increasing the 'max_rounds' parameter."
275 ));
276 }
277
278 factories.sort_unstable_by(|f1, f2| {
279 f1.normalized_qubits()
280 .total_cmp(&f2.normalized_qubits())
281 .then(f1.duration().cmp(&f2.duration()))
282 });
283
284 Ok(factories)
285 }
286
287 fn create_trivial_distillation_unit<'a>(
288 code: &PythonQEC<'py>,
289 qubit: &Bound<'py, PyDict>,
290 max_code_parameter: &SerializableBound<'py>,
291 trivial_distillation_unit: &Bound<'py, PyAny>,
292 input_error_rate: f64,
293 ) -> Result<Vec<std::borrow::Cow<'a, PythonFactory<'py>>>, String> {
294 let result = trivial_distillation_unit
295 .call1((
296 code.bound(),
297 std::convert::AsRef::<pyo3::Py<PyAny>>::as_ref(qubit),
298 &**max_code_parameter,
299 ))
300 .map_err(|e| e.to_string())?;
301
302 let unit =
303 PythonDistillationUnit::new(result.cast_into::<PyDict>().map_err(|e| e.to_string())?)
304 .map_err(|e| e.to_string())?;
305
306 let round = DistillationRound::new(&unit, 0.0, 0);
307
308 let factory =
309 RoundBasedFactory::new(1, 0.0, vec![round], vec![input_error_rate; 2], vec![0.0; 2]);
310
311 Ok(vec![Cow::Owned(PythonFactory::try_from(factory)?)])
312 }
313}
314
315impl<'py> FactoryBuilder<PythonQEC<'py>> for PythonFactoryBuilder<'py> {
316 type Factory = PythonFactory<'py>;
317
318 fn find_factories(
319 &self,
320 code: &PythonQEC<'py>,
321 qubit: &std::rc::Rc<Bound<'py, PyDict>>,
322 _magic_state_type: usize,
323 output_error_rate: f64,
324 max_code_parameter: &SerializableBound<'py>,
325 ) -> Result<Vec<std::borrow::Cow<'_, Self::Factory>>, String> {
326 match &self.implementation {
327 FactoryImplementation::Generic(find_factories) => {
328 Self::create_generic_factories(code, qubit, output_error_rate, find_factories)
329 }
330 FactoryImplementation::RoundBased {
331 distillation_units,
332 trivial_distillation_unit,
333 } => self.create_round_based_factories(
334 code,
335 qubit,
336 output_error_rate,
337 max_code_parameter,
338 distillation_units,
339 trivial_distillation_unit.as_ref(),
340 ),
341 }
342 }
343
344 fn num_magic_state_types(&self) -> usize {
345 self.num_magic_state_types
346 }
347}
348
349#[derive(Clone, Serialize)]
350pub struct PythonFactory<'py> {
351 #[serde(flatten)]
352 values: Map<String, Value>,
353 #[serde(skip)]
354 physical_qubits: u64,
355 #[serde(skip)]
356 duration: u64,
357 #[serde(skip)]
358 num_output_states: u64,
359 #[serde(skip)]
360 phantom: PhantomData<&'py ()>,
361}
362
363impl<'py> PythonFactory<'py> {
364 fn new(values: Map<String, Value>) -> Option<Self> {
365 let physical_qubits = values
366 .get("physical_qubits")
367 .and_then(serde_json::Value::as_u64)?;
368 let duration = values.get("duration").and_then(serde_json::Value::as_u64)?;
369 let num_output_states = values
370 .get("num_output_states")
371 .and_then(serde_json::Value::as_u64)
372 .unwrap_or(1);
373
374 Some(Self {
375 values,
376 physical_qubits,
377 duration,
378 num_output_states,
379 phantom: PhantomData,
380 })
381 }
382
383 fn from_py_dict(dict: &Bound<'py, PyDict>) -> Option<Self> {
384 Self::new(python_dict_to_json_map(dict)?)
385 }
386
387 #[allow(clippy::cast_precision_loss)] // Relevant numbers in arithmetic are small enough
388 pub fn normalized_qubits(&self) -> f64 {
389 self.physical_qubits() as f64 / self.num_output_states() as f64
390 }
391}
392
393impl<'py> TryFrom<RoundBasedFactory<SerializableBound<'py>>> for PythonFactory<'py> {
394 type Error = String;
395
396 fn try_from(value: RoundBasedFactory<SerializableBound<'py>>) -> Result<Self, Self::Error> {
397 let values = json! {{
398 "physical_qubits": value.physical_qubits(),
399 "duration": value.duration(),
400 "num_rounds": value.num_rounds(),
401 "num_output_states": value.num_output_states(),
402 "num_units_per_round": value.num_units_per_round(),
403 "num_input_states": value.num_input_states(),
404 "physical_qubits_per_round": value.physical_qubits_per_round(),
405 "duration_per_round": value.duration_per_round(),
406 "unit_name_per_round": value.unit_names(),
407 "code_parameter_per_round": value
408 .code_parameter_per_round()
409 .iter()
410 .map(|p| p.map_or(Value::Null, |p| json!(p)))
411 .collect::<Vec<_>>(),
412 "logical_error_rate": value.output_error_rate()
413 }};
414
415 Self::new(values.as_object().expect("values is a JSON object").clone()).ok_or(String::from(
416 "Failed to construct factory instance from round-based factory information",
417 ))
418 }
419}
420
421impl<'py> Factory for PythonFactory<'py> {
422 type Parameter = SerializableBound<'py>;
423
424 fn physical_qubits(&self) -> u64 {
425 self.physical_qubits
426 }
427
428 fn duration(&self) -> u64 {
429 self.duration
430 }
431
432 fn num_output_states(&self) -> u64 {
433 self.num_output_states
434 }
435
436 fn max_code_parameter(&self) -> Option<Cow<'_, Self::Parameter>> {
437 None
438 }
439}
440