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/resource_estimator/src/estimates/factory/round_based.rs

372lines · modecode

1use probability::{distribution::Inverse, prelude::Binomial};
2use std::borrow::Cow;
3
4use super::Factory;
5
6pub trait DistillationUnit<P> {
7 fn num_output_states(&self) -> u64;
8 fn num_input_states(&self) -> u64;
9 fn duration(&self, position: usize) -> u64;
10 fn physical_qubits(&self, position: usize) -> u64;
11 fn name(&self) -> &str;
12 fn code_parameter(&self) -> Option<&P>;
13 fn output_error_rate(&self, input_error_rate: f64) -> f64;
14 fn failure_probability(&self, input_error_rate: f64) -> f64;
15}
16
17#[derive(Debug)]
18pub enum FactoryBuildError {
19 LowFailureProbability,
20 HighFailureProbability,
21 OutputErrorRateHigherThanInputErrorRate,
22 UnreasonableHighNumberOfUnitsRequired,
23}
24
25/// One round of distillation in a factory
26///
27/// All units per round are the same. The initial number of units is 1 and can
28/// be iteratively adjusted to match some external constraints.
29#[derive(Debug, Clone)]
30pub struct DistillationRound<P> {
31 num_units: u64,
32 failure_probability_requirement: f64,
33 num_output_states: u64,
34 num_input_states: u64,
35 duration: u64,
36 physical_qubits: u64,
37 name: String,
38 code_parameter: Option<P>,
39}
40
41impl<P: Clone> DistillationRound<P> {
42 pub fn new(
43 unit: &impl DistillationUnit<P>,
44 failure_probability_requirement: f64,
45 position: usize,
46 ) -> Self {
47 Self {
48 num_units: 1,
49 failure_probability_requirement,
50 num_output_states: unit.num_output_states(),
51 num_input_states: unit.num_input_states(),
52 duration: unit.duration(position),
53 physical_qubits: unit.physical_qubits(position),
54 name: unit.name().into(),
55 code_parameter: unit.code_parameter().cloned(),
56 }
57 }
58
59 pub fn adjust_num_units_to(
60 &mut self,
61 output_states_needed_next: u64,
62 failure_probability: f64,
63 ) -> Result<(), FactoryBuildError> {
64 // initial value
65 self.num_units = ((output_states_needed_next as f64)
66 / (self.max_num_output_states() as f64))
67 .ceil() as u64;
68
69 loop {
70 let num_output_states = self.compute_num_output_states(failure_probability);
71 if num_output_states < output_states_needed_next {
72 self.num_units *= 2;
73
74 // TFactory distillation round requires unreasonably high number of units?
75 if self.num_units >= 1_000_000_000_000_000 {
76 return Err(FactoryBuildError::UnreasonableHighNumberOfUnitsRequired);
77 }
78 } else {
79 break;
80 }
81 }
82
83 let mut upper = self.num_units;
84 let mut lower = self.num_units / 2;
85 while lower < upper {
86 self.num_units = u64::midpoint(lower, upper);
87 let num_output_ts = self.compute_num_output_states(failure_probability);
88 if num_output_ts >= output_states_needed_next {
89 upper = self.num_units;
90 } else {
91 lower = self.num_units + 1;
92 }
93 }
94 self.num_units = upper;
95
96 Ok(())
97 }
98
99 pub fn physical_qubits(&self) -> u64 {
100 self.num_units * self.physical_qubits
101 }
102
103 pub fn duration(&self) -> u64 {
104 self.duration
105 }
106
107 pub fn compute_num_output_states(&self, failure_probability: f64) -> u64 {
108 // special case when not necessary to run actual distillation:
109 // the physcial qubit error rate is already below the threshold
110 if failure_probability == 0.0 && self.failure_probability_requirement == 0.0 {
111 return self.num_units * self.num_output_states;
112 }
113 let dist = Binomial::with_failure(self.num_units as usize, failure_probability);
114 dist.inverse(self.failure_probability_requirement) as u64 * self.num_output_states
115 }
116
117 fn max_num_output_states(&self) -> u64 {
118 self.num_units * self.num_output_states
119 }
120
121 pub fn num_units(&self) -> u64 {
122 self.num_units
123 }
124}
125
126#[derive(Debug, Clone)]
127pub struct RoundBasedFactory<P> {
128 length: usize,
129 failure_probability_requirement: f64,
130 rounds: Vec<DistillationRound<P>>,
131 input_error_rate_before_each_round: Vec<f64>,
132 failure_probability_after_each_round: Vec<f64>,
133 physical_qubit_calculation: PhysicalQubitCalculation,
134}
135
136impl<P: Clone> RoundBasedFactory<P> {
137 #[must_use]
138 pub fn new(
139 length: usize,
140 failure_probability_requirement: f64,
141 rounds: Vec<DistillationRound<P>>,
142 input_error_rate_before_each_round: Vec<f64>,
143 failure_probability_after_each_round: Vec<f64>,
144 ) -> Self {
145 Self {
146 length,
147 failure_probability_requirement,
148 rounds,
149 input_error_rate_before_each_round,
150 failure_probability_after_each_round,
151 physical_qubit_calculation: PhysicalQubitCalculation::default(),
152 }
153 }
154
155 pub fn build(
156 units: &[&impl DistillationUnit<P>],
157 initial_input_error_rate: f64,
158 failure_probability_requirement: f64,
159 ) -> Result<RoundBasedFactory<P>, FactoryBuildError> {
160 let rounds: Vec<DistillationRound<P>> = Vec::with_capacity(units.len());
161 let mut input_error_rate_before_each_round = Vec::with_capacity(units.len() + 1);
162 input_error_rate_before_each_round.push(initial_input_error_rate);
163 let failure_probability_after_each_round: Vec<f64> = vec![1.0; units.len() + 1];
164
165 let mut pipeline = Self {
166 length: units.len(),
167 failure_probability_requirement,
168 rounds,
169 input_error_rate_before_each_round,
170 failure_probability_after_each_round,
171 physical_qubit_calculation: PhysicalQubitCalculation::default(),
172 };
173
174 pipeline.compute_units_per_round(units, 1)?;
175
176 Ok(pipeline)
177 }
178
179 fn add_rounds(&mut self, units: &[&impl DistillationUnit<P>]) -> Result<(), FactoryBuildError> {
180 for unit in units {
181 let failure_probability_requirement =
182 self.failure_probability_requirement / (self.length as f64);
183 let &input_error_rate = self
184 .input_error_rate_before_each_round
185 .last()
186 .unwrap_or_else(|| unreachable!());
187 let output_error_rate = unit.output_error_rate(input_error_rate);
188 if output_error_rate > input_error_rate {
189 return Err(FactoryBuildError::OutputErrorRateHigherThanInputErrorRate);
190 }
191 let round =
192 DistillationRound::new(*unit, failure_probability_requirement, self.rounds.len());
193 self.rounds.push(round);
194 self.input_error_rate_before_each_round
195 .push(output_error_rate);
196 }
197
198 Ok(())
199 }
200
201 #[must_use]
202 pub fn physical_qubit_calculation(&self) -> PhysicalQubitCalculation {
203 self.physical_qubit_calculation
204 }
205
206 pub fn set_physical_qubit_calculation(
207 &mut self,
208 physical_qubit_calculation: PhysicalQubitCalculation,
209 ) {
210 self.physical_qubit_calculation = physical_qubit_calculation;
211 }
212
213 #[must_use]
214 pub fn rounds(&self) -> &[DistillationRound<P>] {
215 &self.rounds
216 }
217
218 /// Number of distillation rounds
219 #[must_use]
220 pub fn num_rounds(&self) -> u64 {
221 self.length as u64
222 }
223
224 /// Number of units per distillation round
225 #[must_use]
226 pub fn num_units_per_round(&self) -> Vec<u64> {
227 self.rounds.iter().map(|round| round.num_units).collect()
228 }
229
230 /// Physical qubits per round
231 pub fn physical_qubits_per_round(&self) -> Vec<u64> {
232 self.rounds
233 .iter()
234 .map(DistillationRound::physical_qubits)
235 .collect()
236 }
237
238 /// Runtime in ns per round
239 pub fn duration_per_round(&self) -> Vec<u64> {
240 self.rounds
241 .iter()
242 .map(DistillationRound::duration)
243 .collect()
244 }
245
246 /// Names of distillation units per round
247 #[must_use]
248 pub fn unit_names(&self) -> Vec<String> {
249 self.rounds.iter().map(|round| round.name.clone()).collect()
250 }
251
252 /// This computes the necessary number of units per round in order to
253 /// achieve the required success probability
254 /// Returning None means that the sequence of units does not provide a TFactory with the required output error rate.
255 #[allow(clippy::doc_markdown)]
256 pub fn compute_units_per_round(
257 &mut self,
258 units: &[&impl DistillationUnit<P>],
259 multiplier: u64,
260 ) -> Result<(), FactoryBuildError> {
261 self.add_rounds(units)?;
262
263 if self.length > 0 {
264 let mut states_needed_next =
265 self.rounds[self.length - 1].num_output_states * multiplier;
266
267 for idx in (0..self.length).rev() {
268 let q =
269 units[idx].failure_probability(self.input_error_rate_before_each_round[idx]);
270 if q <= 0.0 {
271 return Err(FactoryBuildError::LowFailureProbability);
272 }
273
274 if q >= 1.0 {
275 return Err(FactoryBuildError::HighFailureProbability);
276 }
277
278 self.failure_probability_after_each_round[idx] = q;
279 self.rounds[idx].adjust_num_units_to(states_needed_next, q)?;
280
281 states_needed_next =
282 self.rounds[idx].num_input_states * self.rounds[idx].num_units();
283 }
284 }
285
286 Ok(())
287 }
288
289 #[must_use]
290 pub fn input_error_rate(&self) -> f64 {
291 // Even when there are no units `input_error_rate_before_each_round`
292 // has one element
293 self.input_error_rate_before_each_round[0]
294 }
295
296 #[must_use]
297 pub fn output_error_rate(&self) -> f64 {
298 self.input_error_rate_before_each_round[self.length]
299 }
300
301 #[must_use]
302 pub fn num_input_states(&self) -> u64 {
303 self.rounds
304 .first()
305 .map_or(0, |round| round.num_input_states * round.num_units())
306 }
307
308 #[must_use]
309 pub fn normalized_qubits(&self) -> f64 {
310 (self.physical_qubits() as f64) / (self.num_output_states() as f64)
311 }
312
313 /// Code parameter per round
314 #[must_use]
315 pub fn code_parameter_per_round(&self) -> Vec<Option<&P>> {
316 self.rounds
317 .iter()
318 .map(|round| round.code_parameter.as_ref())
319 .collect()
320 }
321}
322
323impl<P: Clone> Factory for RoundBasedFactory<P> {
324 type Parameter = P;
325
326 fn physical_qubits(&self) -> u64 {
327 match self.physical_qubit_calculation {
328 PhysicalQubitCalculation::Max => self
329 .rounds
330 .iter()
331 .map(DistillationRound::physical_qubits)
332 .max()
333 .unwrap_or(0),
334 PhysicalQubitCalculation::Sum => self
335 .rounds
336 .iter()
337 .map(DistillationRound::physical_qubits)
338 .sum::<u64>(),
339 }
340 }
341
342 fn duration(&self) -> u64 {
343 self.rounds.iter().map(DistillationRound::duration).sum()
344 }
345
346 fn num_output_states(&self) -> u64 {
347 let last_round = self
348 .rounds
349 .last()
350 .expect("at least one round should be present");
351 let failure_probability = self.failure_probability_after_each_round[self.length - 1];
352 // This should not fail, as we already evaluated this
353 // failure_probability when building the factory
354 last_round.compute_num_output_states(failure_probability)
355 }
356
357 fn max_code_parameter(&self) -> Option<Cow<P>> {
358 self.code_parameter_per_round()
359 .last()
360 .expect("at least one round should be present")
361 .map(|f| Cow::Borrowed(f))
362 }
363}
364
365#[derive(Copy, Clone, Debug, Default)]
366pub enum PhysicalQubitCalculation {
367 /// physical qubits can be shared among rounds
368 #[default]
369 Max,
370 /// each round has its own physical qubits
371 Sum,
372}
373