microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.18.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/generic_estimator/factory.rs

354lines · modecode

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