microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.22.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/generic_estimator/factory.rs

431lines · 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((code.bound(), qubit.as_ref(), output_error_rate))
156 .map_err(|e| e.to_string())?;
157
158 if result.is_none() {
159 Ok(vec![])
160 } else {
161 let factories = result.downcast::<PyList>().map_err(|e| e.to_string())?;
162 let mut converted = vec![];
163
164 for element in factories {
165 let dict = element.downcast::<PyDict>().map_err(|e| e.to_string())?;
166 let factory = PythonFactory::from_py_dict(dict).ok_or(format!(
167 "Failed to convert factory from Python dict: {dict:?}, does the dictionary contain entries for 'physical_qubits' and 'duration'?",
168 ))?;
169 converted.push(Cow::Owned(factory));
170 }
171
172 Ok(converted)
173 }
174 }
175
176 fn create_round_based_factories(
177 &self,
178 code: &PythonQEC<'py>,
179 qubit: &Bound<'py, PyDict>,
180 output_error_rate: f64,
181 max_code_parameter: &SerializableBound<'py>,
182 distillation_units: &Bound<'py, PyAny>,
183 trivial_distillation_unit: Option<&Bound<'py, PyAny>>,
184 ) -> Result<Vec<std::borrow::Cow<PythonFactory<'py>>>, String> {
185 // input error rate
186 let qubit_key = self
187 .builder
188 .getattr("gate_error")
189 .and_then(|a| a.extract())
190 .unwrap_or(String::from("gate_error"));
191 let initial_input_error_rate = qubit
192 .get_item(&qubit_key)
193 .map_err(|e| e.to_string())?
194 .extract()
195 .map_err(|e| e.to_string())?;
196
197 if initial_input_error_rate <= output_error_rate
198 && let Some(trivial_distillation_unit) = trivial_distillation_unit
199 {
200 return Self::create_trivial_distillation_unit(
201 code,
202 qubit,
203 max_code_parameter,
204 trivial_distillation_unit,
205 initial_input_error_rate,
206 );
207 }
208
209 let use_max_qubits_per_round = self
210 .builder
211 .getattr("use_max_qubits_per_round")
212 .and_then(|a| a.extract())
213 .unwrap_or(false);
214 let max_rounds = self
215 .builder
216 .getattr("max_rounds")
217 .and_then(|a| a.extract())
218 .unwrap_or(3);
219 let max_extra_rounds = self
220 .builder
221 .getattr("max_extra_rounds")
222 .and_then(|a| a.extract())
223 .unwrap_or(5);
224
225 let return_value = distillation_units
226 .call1((code.bound(), qubit.as_ref(), &**max_code_parameter))
227 .map_err(|e| e.to_string())?;
228
229 let units: Vec<_> = return_value
230 .try_iter()
231 .map_err(|e| {
232 format!("{e} (check the return value of the 'distillation_units' method)",)
233 })?
234 .map(|bound| PythonDistillationUnit::new(bound?.downcast_into::<PyDict>()?))
235 .collect::<Result<_, _>>()
236 .map_err(|e| e.to_string())?;
237
238 let mut factories: Vec<Cow<PythonFactory>> = vec![];
239
240 ordered_bfs(&units, max_extra_rounds, |selected_units| {
241 if selected_units.len() > max_rounds && !factories.is_empty() {
242 return Ok(OrderedBFSControl::Terminate);
243 }
244
245 if let Ok(mut factory) =
246 RoundBasedFactory::build(&selected_units, initial_input_error_rate, 0.01)
247 {
248 if factory.output_error_rate() <= output_error_rate {
249 factory.set_physical_qubit_calculation(if use_max_qubits_per_round {
250 PhysicalQubitCalculation::Max
251 } else {
252 PhysicalQubitCalculation::Sum
253 });
254 factories.push(Cow::Owned(PythonFactory::try_from(factory)?));
255
256 if selected_units.len() > max_rounds {
257 return Ok(OrderedBFSControl::Terminate);
258 }
259 }
260 }
261
262 Ok(OrderedBFSControl::Continue)
263 })?;
264
265 if factories.is_empty() {
266 return Err(format!(
267 "No factories found for output error rate: {output_error_rate}, try increasing the 'max_rounds' parameter."
268 ));
269 }
270
271 factories.sort_unstable_by(|f1, f2| {
272 f1.normalized_qubits()
273 .total_cmp(&f2.normalized_qubits())
274 .then(f1.duration().cmp(&f2.duration()))
275 });
276
277 Ok(factories)
278 }
279
280 fn create_trivial_distillation_unit<'a>(
281 code: &PythonQEC<'py>,
282 qubit: &Bound<'py, PyDict>,
283 max_code_parameter: &SerializableBound<'py>,
284 trivial_distillation_unit: &Bound<'py, PyAny>,
285 input_error_rate: f64,
286 ) -> Result<Vec<std::borrow::Cow<'a, PythonFactory<'py>>>, String> {
287 let result = trivial_distillation_unit
288 .call1((code.bound(), qubit.as_ref(), &**max_code_parameter))
289 .map_err(|e| e.to_string())?;
290
291 let unit = PythonDistillationUnit::new(
292 result
293 .downcast_into::<PyDict>()
294 .map_err(|e| e.to_string())?,
295 )
296 .map_err(|e| e.to_string())?;
297
298 let round = DistillationRound::new(&unit, 0.0, 0);
299
300 let factory =
301 RoundBasedFactory::new(1, 0.0, vec![round], vec![input_error_rate; 2], vec![0.0; 2]);
302
303 Ok(vec![Cow::Owned(PythonFactory::try_from(factory)?)])
304 }
305}
306
307impl<'py> FactoryBuilder<PythonQEC<'py>> for PythonFactoryBuilder<'py> {
308 type Factory = PythonFactory<'py>;
309
310 fn find_factories(
311 &self,
312 code: &PythonQEC<'py>,
313 qubit: &std::rc::Rc<Bound<'py, PyDict>>,
314 _magic_state_type: usize,
315 output_error_rate: f64,
316 max_code_parameter: &SerializableBound<'py>,
317 ) -> Result<Vec<std::borrow::Cow<Self::Factory>>, String> {
318 match &self.implementation {
319 FactoryImplementation::Generic(find_factories) => {
320 Self::create_generic_factories(code, qubit, output_error_rate, find_factories)
321 }
322 FactoryImplementation::RoundBased {
323 distillation_units,
324 trivial_distillation_unit,
325 } => self.create_round_based_factories(
326 code,
327 qubit,
328 output_error_rate,
329 max_code_parameter,
330 distillation_units,
331 trivial_distillation_unit.as_ref(),
332 ),
333 }
334 }
335
336 fn num_magic_state_types(&self) -> usize {
337 self.num_magic_state_types
338 }
339}
340
341#[derive(Clone, Serialize)]
342pub struct PythonFactory<'py> {
343 #[serde(flatten)]
344 values: Map<String, Value>,
345 #[serde(skip)]
346 physical_qubits: u64,
347 #[serde(skip)]
348 duration: u64,
349 #[serde(skip)]
350 num_output_states: u64,
351 #[serde(skip)]
352 phantom: PhantomData<&'py ()>,
353}
354
355impl<'py> PythonFactory<'py> {
356 fn new(values: Map<String, Value>) -> Option<Self> {
357 let physical_qubits = values
358 .get("physical_qubits")
359 .and_then(serde_json::Value::as_u64)?;
360 let duration = values.get("duration").and_then(serde_json::Value::as_u64)?;
361 let num_output_states = values
362 .get("num_output_states")
363 .and_then(serde_json::Value::as_u64)
364 .unwrap_or(1);
365
366 Some(Self {
367 values,
368 physical_qubits,
369 duration,
370 num_output_states,
371 phantom: PhantomData,
372 })
373 }
374
375 fn from_py_dict(dict: &Bound<'py, PyDict>) -> Option<Self> {
376 Self::new(python_dict_to_json_map(dict)?)
377 }
378
379 #[allow(clippy::cast_precision_loss)] // Relevant numbers in arithmetic are small enough
380 pub fn normalized_qubits(&self) -> f64 {
381 self.physical_qubits() as f64 / self.num_output_states() as f64
382 }
383}
384
385impl<'py> TryFrom<RoundBasedFactory<SerializableBound<'py>>> for PythonFactory<'py> {
386 type Error = String;
387
388 fn try_from(value: RoundBasedFactory<SerializableBound<'py>>) -> Result<Self, Self::Error> {
389 let values = json! {{
390 "physical_qubits": value.physical_qubits(),
391 "duration": value.duration(),
392 "num_rounds": value.num_rounds(),
393 "num_output_states": value.num_output_states(),
394 "num_units_per_round": value.num_units_per_round(),
395 "num_input_states": value.num_input_states(),
396 "physical_qubits_per_round": value.physical_qubits_per_round(),
397 "duration_per_round": value.duration_per_round(),
398 "unit_name_per_round": value.unit_names(),
399 "code_parameter_per_round": value
400 .code_parameter_per_round()
401 .iter()
402 .map(|p| p.map_or(Value::Null, |p| json!(p)))
403 .collect::<Vec<_>>(),
404 "logical_error_rate": value.output_error_rate()
405 }};
406
407 Self::new(values.as_object().expect("values is a JSON object").clone()).ok_or(String::from(
408 "Failed to construct factory instance from round-based factory information",
409 ))
410 }
411}
412
413impl<'py> Factory for PythonFactory<'py> {
414 type Parameter = SerializableBound<'py>;
415
416 fn physical_qubits(&self) -> u64 {
417 self.physical_qubits
418 }
419
420 fn duration(&self) -> u64 {
421 self.duration
422 }
423
424 fn num_output_states(&self) -> u64 {
425 self.num_output_states
426 }
427
428 fn max_code_parameter(&self) -> Option<Cow<Self::Parameter>> {
429 None
430 }
431}
432