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/qsharp/estimator/_estimator.py

1082lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3import re
4from typing import Any, Dict, List, Optional, Union
5from dataclasses import dataclass, field
6from .._native import physical_estimates
7
8import json
9
10try:
11 # Both markdown and mdx_math (from python-markdown-math) must be present for our markdown
12 # rendering logic to work. If either is missing, we'll fall back to plain text.
13 import markdown
14 import mdx_math
15
16 has_markdown = True
17except ImportError:
18 has_markdown = False
19
20
21class EstimatorError(BaseException):
22 """
23 An error returned from the resource estimation.
24 """
25
26 def __init__(self, code: str, message: str):
27 self.message = f"Error estimating resources ({code}):\n{message}"
28 self.code = code
29
30 def __str__(self):
31 return self.message
32
33
34@dataclass
35class AutoValidatingParams:
36 """
37 A helper class for target parameters.
38
39 It has a function as_dict that automatically extracts a dictionary from
40 the class' fields. They are added to the result dictionary if their value
41 is not None, the key is automatically transformed from Python snake case
42 to camel case, and if validate is True and if the field has a validation
43 function, the field is validated beforehand.
44 """
45
46 def as_dict(self, validate=True):
47 result = {}
48
49 for name, field in self.__dataclass_fields__.items():
50 field_value = self.__getattribute__(name)
51 if field_value is not None:
52 # validate field?
53 if validate and "validate" in field.metadata:
54 func = field.metadata["validate"]
55 # check for indirect call (like in @staticmethod)
56 if hasattr(func, "__func__"):
57 func = func.__func__
58 func(name, field_value)
59
60 # translate field name to camel case
61 s = re.sub(r"(_|-)+", " ", name).title().replace(" ", "")
62 attribute = "".join([s[0].lower(), s[1:]])
63 result[attribute] = field_value
64
65 if validate:
66 self.post_validation(result)
67
68 return result
69
70 def post_validation(self, result):
71 """
72 A function that is called after all individual fields have been
73 validated, but before the result is returned.
74
75 Here result is the current dictionary.
76 """
77 pass
78
79
80def validating_field(validation_func, default=None):
81 """
82 A helper method to declare field for an AutoValidatingParams data class.
83 """
84 return field(default=default, metadata={"validate": validation_func})
85
86
87class QubitParams:
88 GATE_US_E3 = "qubit_gate_us_e3"
89 GATE_US_E4 = "qubit_gate_us_e4"
90 GATE_NS_E3 = "qubit_gate_ns_e3"
91 GATE_NS_E4 = "qubit_gate_ns_e4"
92 MAJ_NS_E4 = "qubit_maj_ns_e4"
93 MAJ_NS_E6 = "qubit_maj_ns_e6"
94
95
96class QECScheme:
97 SURFACE_CODE = "surface_code"
98 FLOQUET_CODE = "floquet_code"
99
100
101def _check_error_rate(name, value):
102 if value <= 0.0 or value >= 1.0:
103 raise ValueError(f"{name} must be between 0 and 1")
104
105
106def _check_error_rate_or_process_and_readout(name, value):
107 if value is None:
108 return
109
110 if isinstance(value, float):
111 _check_error_rate(name, value)
112 return
113
114 if not isinstance(value, MeasurementErrorRate):
115 raise ValueError(
116 f"{name} must be either a float or "
117 "MeasurementErrorRate with two fields: 'process' and 'readout'"
118 )
119
120
121def check_time(name, value):
122 pat = r"^(\+?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)\s*(s|ms|μs|µs|us|ns)$"
123 if re.match(pat, value) is None:
124 raise ValueError(
125 f"{name} is not a valid time string; use a " "suffix s, ms, us, or ns"
126 )
127
128
129@dataclass
130class MeasurementErrorRate(AutoValidatingParams):
131 process: float = field(metadata={"validate": _check_error_rate})
132 readout: float = field(metadata={"validate": _check_error_rate})
133
134
135@dataclass
136class EstimatorQubitParams(AutoValidatingParams):
137 @staticmethod
138 def check_instruction_set(name, value):
139 if value not in [
140 "gate-based",
141 "gate_based",
142 "GateBased",
143 "gateBased",
144 "Majorana",
145 "majorana",
146 ]:
147 raise ValueError(f"{name} must be GateBased or Majorana")
148
149 name: Optional[str] = None
150 instruction_set: Optional[str] = validating_field(check_instruction_set)
151 one_qubit_measurement_time: Optional[str] = validating_field(check_time)
152 two_qubit_joint_measurement_time: Optional[str] = validating_field(check_time)
153 one_qubit_gate_time: Optional[str] = validating_field(check_time)
154 two_qubit_gate_time: Optional[str] = validating_field(check_time)
155 t_gate_time: Optional[str] = validating_field(check_time)
156 one_qubit_measurement_error_rate: Union[None, float, MeasurementErrorRate] = (
157 validating_field(_check_error_rate_or_process_and_readout)
158 )
159 two_qubit_joint_measurement_error_rate: Union[None, float, MeasurementErrorRate] = (
160 validating_field(_check_error_rate_or_process_and_readout)
161 )
162 one_qubit_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
163 two_qubit_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
164 t_gate_error_rate: Optional[float] = validating_field(_check_error_rate)
165 idle_error_rate: Optional[float] = validating_field(_check_error_rate)
166
167 _default_models = [
168 QubitParams.GATE_US_E3,
169 QubitParams.GATE_US_E4,
170 QubitParams.GATE_NS_E3,
171 QubitParams.GATE_NS_E4,
172 QubitParams.MAJ_NS_E4,
173 QubitParams.MAJ_NS_E6,
174 ]
175 _gate_based = ["gate-based", "gate_based", "GateBased", "gateBased"]
176 _maj_based = ["Majorana", "majorana"]
177
178 def post_validation(self, result):
179 # check whether all fields have been specified in case a custom qubit
180 # model is specified
181 custom = result != {} and (
182 self.name is None or self.name not in self._default_models
183 )
184
185 # no further validation needed for non-custom models
186 if not custom:
187 return
188
189 # instruction set must be set
190 if self.instruction_set is None:
191 raise LookupError(
192 "instruction_set must be set for custom qubit " "parameters"
193 )
194
195 # NOTE at this point, we know that instruction set must have valid
196 # value
197 if self.one_qubit_measurement_time is None:
198 raise LookupError("one_qubit_measurement_time must be set")
199 if self.one_qubit_measurement_error_rate is None:
200 raise LookupError("one_qubit_measurement_error_rate must be set")
201
202 # this only needs to be checked for gate based qubits
203 if self.instruction_set in self._gate_based:
204 if self.one_qubit_gate_time is None:
205 raise LookupError("one_qubit_gate_time must be set")
206
207 def as_dict(self, validate=True) -> Dict[str, Any]:
208 qubit_params = super().as_dict(validate)
209 if len(qubit_params) != 0:
210 if isinstance(self.one_qubit_measurement_error_rate, MeasurementErrorRate):
211 qubit_params["oneQubitMeasurementErrorRate"] = (
212 self.one_qubit_measurement_error_rate.as_dict(validate)
213 )
214
215 if isinstance(
216 self.two_qubit_joint_measurement_error_rate, MeasurementErrorRate
217 ):
218 qubit_params["twoQubitJointMeasurementErrorRate"] = (
219 self.two_qubit_joint_measurement_error_rate.as_dict(validate)
220 )
221
222 return qubit_params
223
224
225@dataclass
226class EstimatorQecScheme(AutoValidatingParams):
227 name: Optional[str] = None
228 error_correction_threshold: Optional[float] = validating_field(_check_error_rate)
229 crossing_prefactor: Optional[float] = None
230 distance_coefficient_power: Optional[int] = None
231 logical_cycle_time: Optional[str] = None
232 physical_qubits_per_logical_qubit: Optional[str] = None
233 max_code_distance: Optional[int] = None
234
235
236@dataclass
237class ProtocolSpecificDistillationUnitSpecification(AutoValidatingParams):
238 num_unit_qubits: Optional[int] = None
239 duration_in_qubit_cycle_time: Optional[int] = None
240
241 def post_validation(self, result):
242 if self.num_unit_qubits is None:
243 raise LookupError("num_unit_qubits must be set")
244
245 if self.duration_in_qubit_cycle_time is None:
246 raise LookupError("duration_in_qubit_cycle_time must be set")
247
248
249@dataclass
250class DistillationUnitSpecification(AutoValidatingParams):
251 name: Optional[str] = None
252 display_name: Optional[str] = None
253 num_input_ts: Optional[int] = None
254 num_output_ts: Optional[int] = None
255 failure_probability_formula: Optional[str] = None
256 output_error_rate_formula: Optional[str] = None
257 physical_qubit_specification: Optional[
258 ProtocolSpecificDistillationUnitSpecification
259 ] = None
260 logical_qubit_specification: Optional[
261 ProtocolSpecificDistillationUnitSpecification
262 ] = None
263 logical_qubit_specification_first_round_override: Optional[
264 ProtocolSpecificDistillationUnitSpecification
265 ] = None
266
267 def has_custom_specification(self):
268 return (
269 self.display_name is not None
270 or self.num_input_ts is not None
271 or self.num_output_ts is not None
272 or self.failure_probability_formula is not None
273 or self.output_error_rate_formula is not None
274 or self.physical_qubit_specification is not None
275 or self.logical_qubit_specification is not None
276 or self.logical_qubit_specification_first_round_override is not None
277 )
278
279 def has_predefined_name(self):
280 return self.name is not None
281
282 def post_validation(self, result):
283 if not self.has_custom_specification() and not self.has_predefined_name():
284 raise LookupError(
285 "name must be set or custom specification must be provided"
286 )
287
288 if self.has_custom_specification() and self.has_predefined_name():
289 raise LookupError(
290 "If predefined name is provided, "
291 "custom specification is not allowed. "
292 "Either remove name or remove all other "
293 "specification of the distillation unit"
294 )
295
296 if self.has_predefined_name():
297 return # all other validation is on the server side
298
299 if self.num_input_ts is None:
300 raise LookupError("num_input_ts must be set")
301
302 if self.num_output_ts is None:
303 raise LookupError("num_output_ts must be set")
304
305 if self.failure_probability_formula is None:
306 raise LookupError("failure_probability_formula must be set")
307
308 if self.output_error_rate_formula is None:
309 raise LookupError("output_error_rate_formula must be set")
310
311 if self.physical_qubit_specification is not None:
312 self.physical_qubit_specification.post_validation(result)
313
314 if self.logical_qubit_specification is not None:
315 self.logical_qubit_specification.post_validation(result)
316
317 if self.logical_qubit_specification_first_round_override is not None:
318 self.logical_qubit_specification_first_round_override.post_validation(
319 result
320 )
321
322 def as_dict(self, validate=True) -> Dict[str, Any]:
323 specification_dict = super().as_dict(validate)
324 if len(specification_dict) != 0:
325 if self.physical_qubit_specification is not None:
326 physical_qubit_specification_dict = (
327 self.physical_qubit_specification.as_dict(validate)
328 )
329 if len(physical_qubit_specification_dict) != 0:
330 specification_dict["physicalQubitSpecification"] = (
331 physical_qubit_specification_dict
332 )
333
334 if self.logical_qubit_specification is not None:
335 logical_qubit_specification_dict = (
336 self.logical_qubit_specification.as_dict(validate)
337 )
338 if len(logical_qubit_specification_dict) != 0:
339 specification_dict["logicalQubitSpecification"] = (
340 logical_qubit_specification_dict
341 )
342
343 if self.logical_qubit_specification_first_round_override is not None:
344 logical_qubit_specification_first_round_override_dict = (
345 self.logical_qubit_specification_first_round_override.as_dict(
346 validate
347 )
348 )
349 if len(logical_qubit_specification_first_round_override_dict) != 0:
350 specification_dict[
351 "logicalQubitSpecificationFirstRoundOverride"
352 ] = logical_qubit_specification_first_round_override_dict
353
354 return specification_dict
355
356
357@dataclass
358class ErrorBudgetPartition(AutoValidatingParams):
359 logical: float = 0.001 / 3
360 t_states: float = 0.001 / 3
361 rotations: float = 0.001 / 3
362
363
364@dataclass
365class EstimatorConstraints(AutoValidatingParams):
366 @staticmethod
367 def at_least_one(name, value):
368 if value < 1:
369 raise ValueError(f"{name} must be at least 1")
370
371 logical_depth_factor: Optional[float] = validating_field(at_least_one)
372 max_t_factories: Optional[int] = validating_field(at_least_one)
373 max_duration: Optional[int] = validating_field(check_time)
374 max_physical_qubits: Optional[int] = validating_field(at_least_one)
375
376 def post_validation(self, result):
377 if self.max_duration is not None and self.max_physical_qubits is not None:
378 raise LookupError(
379 "Both duration and number of physical qubits constraints are provided, but only one is allowe at a time."
380 )
381
382
383class EstimatorInputParamsItem:
384 """
385 Input params for microsoft.estimator target
386
387 :ivar error_budget Total error budget for execution of the algorithm
388 """
389
390 def __init__(self):
391 super().__init__()
392
393 self.qubit_params: EstimatorQubitParams = EstimatorQubitParams()
394 self.qec_scheme: EstimatorQecScheme = EstimatorQecScheme()
395 self.distillation_unit_specifications = (
396 []
397 ) # type: List[DistillationUnitSpecification]
398 self.constraints: EstimatorConstraints = EstimatorConstraints()
399 self.error_budget: Optional[Union[float, ErrorBudgetPartition]] = None
400 self.estimate_type: Optional[str] = None
401
402 def as_dict(self, validate=True, additional_params=None) -> Dict[str, Any]:
403 result = {}
404
405 qubit_params = self.qubit_params.as_dict(validate)
406 if len(qubit_params) != 0:
407 result["qubitParams"] = qubit_params
408 elif hasattr(additional_params, "qubit_params"):
409 qubit_params = additional_params.qubit_params.as_dict(validate)
410 if len(qubit_params) != 0:
411 result["qubitParams"] = qubit_params
412
413 qec_scheme = self.qec_scheme.as_dict(validate)
414 if len(qec_scheme) != 0:
415 result["qecScheme"] = qec_scheme
416 elif hasattr(additional_params, "qec_scheme"):
417 qec_scheme = additional_params.qec_scheme.as_dict(validate)
418 if len(qec_scheme) != 0:
419 result["qecScheme"] = qec_scheme
420
421 for specification in self.distillation_unit_specifications:
422 specification_dict = specification.as_dict(validate)
423 if len(specification_dict) != 0:
424 if result.get("distillationUnitSpecifications") is None:
425 result["distillationUnitSpecifications"] = []
426
427 result["distillationUnitSpecifications"].append(specification_dict)
428 if result.get("distillationUnitSpecifications") is not None and hasattr(
429 additional_params, "distillation_unit_specifications"
430 ):
431 for specification in additional_params.distillation_unit_specifications:
432 specification_dict = specification.as_dict(validate)
433 if len(specification_dict) != 0:
434 if result.get("distillationUnitSpecifications") is None:
435 result["distillationUnitSpecifications"] = []
436
437 result["distillationUnitSpecifications"].append(specification_dict)
438
439 constraints = self.constraints.as_dict(validate)
440 if len(constraints) != 0:
441 result["constraints"] = constraints
442 elif hasattr(additional_params, "constraints"):
443 constraints = additional_params.constraints.as_dict(validate)
444 if len(constraints) != 0:
445 result["constraints"] = constraints
446
447 if self.error_budget is not None:
448 if isinstance(self.error_budget, float) or isinstance(
449 self.error_budget, int
450 ):
451 if validate and (self.error_budget <= 0 or self.error_budget >= 1):
452 message = "error_budget must be value between 0 and 1"
453 raise ValueError(message)
454 result["errorBudget"] = self.error_budget
455 elif isinstance(self.error_budget, ErrorBudgetPartition):
456 result["errorBudget"] = self.error_budget.as_dict(validate)
457 elif hasattr(additional_params, "error_budget"):
458 if isinstance(additional_params.error_budget, float) or isinstance(
459 additional_params.error_budget, int
460 ):
461 if validate and (
462 additional_params.error_budget <= 0
463 or additional_params.error_budget >= 1
464 ):
465 message = "error_budget must be value between 0 and 1"
466 raise ValueError(message)
467 result["errorBudget"] = additional_params.error_budget
468 elif isinstance(additional_params.error_budget, ErrorBudgetPartition):
469 result["errorBudget"] = additional_params.error_budget.as_dict(validate)
470
471 if self.estimate_type is not None:
472 if self.estimate_type not in ["frontier", "singlePoint"]:
473 raise ValueError(
474 "estimate_type must be either 'frontier' or 'singlePoint'"
475 )
476 result["estimateType"] = self.estimate_type
477
478 return result
479
480
481class EstimatorParams(EstimatorInputParamsItem):
482 MAX_NUM_ITEMS: int = 1000
483
484 def __init__(self, num_items: Optional[int] = None):
485 EstimatorInputParamsItem.__init__(self)
486
487 if num_items is not None:
488 self.has_items = True
489 if num_items <= 0 or num_items > self.MAX_NUM_ITEMS:
490 raise ValueError(
491 "num_items must be a positive value less or equal to "
492 f"{self.MAX_NUM_ITEMS}"
493 )
494 self._items = [EstimatorInputParamsItem() for _ in range(num_items)]
495 else:
496 self.has_items = False
497
498 @property
499 def items(self) -> List:
500 if self.has_items:
501 return self._items
502 else:
503 raise Exception(
504 "Cannot access items in a non-batching job, call "
505 "make_params with num_items parameter"
506 )
507
508 def as_dict(self, validate=True) -> Dict[str, Any]:
509 """
510 Constructs a dictionary from the input params.
511
512 For batching jobs, top-level entries are merged into item entries.
513 Item entries have priority in case they are specified.
514 """
515
516 # initialize result and set type hint
517 result: Dict[str, Any] = EstimatorInputParamsItem.as_dict(self, validate)
518
519 if self.has_items:
520 result["items"] = [item.as_dict(validate, self) for item in self._items]
521 # In case of batching, no need to stop if failing an item
522 result["resumeAfterFailedItem"] = True
523
524 return result
525
526
527class HTMLWrapper:
528 """
529 Simple HTML wrapper to expose _repr_html_ for Jupyter clients.
530 """
531
532 def __init__(self, content: str):
533 self.content = content
534
535 def _repr_html_(self):
536 return self.content
537
538
539class EstimatorResult(dict):
540 """
541 Microsoft Resource Estimator result.
542
543 The class represents simple resource estimation results as well as batching
544 resource estimation results. The latter can be indexed by an integer index to
545 access an individual result from the batching result.
546 """
547
548 MAX_DEFAULT_ITEMS_IN_TABLE = 5
549
550 def __init__(self, data: Union[Dict, List]):
551 self._error = None
552
553 if isinstance(data, list) and len(data) == 1:
554 data = data[0]
555 if not EstimatorResult._is_succeeded(data):
556 raise EstimatorError(data["code"], data["message"])
557
558 if isinstance(data, dict):
559 self._data = data
560 super().__init__(data)
561
562 self._is_simple = True
563 if EstimatorResult._is_succeeded(self):
564 self._repr = self._item_result_table()
565 self.summary = HTMLWrapper(self._item_result_summary_table())
566 self.diagram = EstimatorResultDiagram(self.data().copy())
567 else:
568 self._error = EstimatorError(data["code"], data["message"])
569
570 elif isinstance(data, list):
571 super().__init__(
572 {idx: EstimatorResult(item_data) for idx, item_data in enumerate(data)}
573 )
574
575 self._data = data
576 self._is_simple = False
577 num_items = len(data)
578 self._repr = ""
579 if num_items > self.MAX_DEFAULT_ITEMS_IN_TABLE:
580 self._repr += (
581 "<p><b>Info:</b> <i>The overview table is "
582 "cut off after "
583 f"{self.MAX_DEFAULT_ITEMS_IN_TABLE} items. If "
584 "you want to see all items, suffix the result "
585 "variable with <code>[:]</code></i></p>"
586 )
587 num_items = self.MAX_DEFAULT_ITEMS_IN_TABLE
588 self._repr += self._batch_result_table(range(num_items))
589
590 # Add plot function for batching jobs
591 self.plot = self._plot
592 self.summary_data_frame = self._summary_data_frame
593
594 def _is_succeeded(self):
595 return "status" in self and self["status"] == "success"
596
597 def data(self, idx: Optional[int] = None) -> Any:
598 """
599 Returns raw data of the result object.
600
601 In case of a batching job, you can pass an index to access a specific
602 item.
603 """
604 if idx is None:
605 return self._data
606 elif not self._is_simple:
607 return self._data[idx]
608 else:
609 msg = "Cannot pass parameter 'idx' to 'data' for non-batching job"
610 raise ValueError(msg)
611
612 @property
613 def error(self) -> Optional[EstimatorError]:
614 """
615 Returns the error object if the result is an error.
616 """
617 return self._error
618
619 @property
620 def logical_counts(self):
621 """
622 Returns the logical counts of the result.
623 """
624 if self._is_simple:
625 return LogicalCounts(self.data()["logicalCounts"])
626 else:
627 return LogicalCounts(self.data(0)["logicalCounts"])
628
629 def _repr_html_(self):
630 """
631 HTML table representation of the result.
632 """
633 if self._error:
634 raise self._error
635 return self._repr
636
637 def __getitem__(self, key):
638 """
639 If the result represents a batching job and key is a slice, a
640 side-by-side table comparison is shown for the indexes represented by
641 the slice.
642
643 Otherwise, the key is used to access the raw data directly.
644 """
645 if isinstance(key, slice):
646 if self._is_simple:
647 msg = "Cannot pass slice to '__getitem__' for non-batching job"
648 raise ValueError(msg)
649 return HTMLWrapper(self._batch_result_table(range(len(self))[key]))
650 else:
651 if super().__contains__(key):
652 return super().__getitem__(key)
653 elif super().__contains__("frontierEntries"):
654 return super().__getitem__("frontierEntries")[0].__getitem__(key)
655 else:
656 raise KeyError(key)
657
658 def _plot(self, **kwargs):
659 """
660 Plots all result items in a space time plot, where the x-axis shows
661 total runtime, and the y-axis shows total number of physical qubits.
662 Both axes are in log-scale.
663 Attributes:
664 labels (list): List of labels for the legend.
665 """
666 try:
667 import matplotlib.pyplot as plt
668 except ImportError:
669 raise ImportError(
670 "Missing optional 'matplotlib' dependency. To install run: "
671 "pip install matplotlib"
672 )
673
674 labels = kwargs.pop("labels", [])
675
676 [xs, ys] = zip(
677 *[
678 (
679 self.data(i)["physicalCounts"]["runtime"],
680 self.data(i)["physicalCounts"]["physicalQubits"],
681 )
682 for i in range(len(self))
683 ]
684 )
685
686 _ = plt.figure(figsize=(15, 8))
687
688 plt.ylabel("Physical qubits")
689 plt.xlabel("Runtime")
690 plt.loglog()
691 for i, (x, y) in enumerate(zip(xs, ys)):
692 if isinstance(labels, list) and i < len(labels):
693 label = labels[i]
694 else:
695 label = str(i)
696 plt.scatter(x=[x], y=[y], label=label, marker="os+x"[i % 4])
697
698 nsec = 1
699 usec = 1e3 * nsec
700 msec = 1e3 * usec
701 sec = 1e3 * msec
702 min = 60 * sec
703 hour = 60 * min
704 day = 24 * hour
705 week = 7 * day
706 month = 31 * day
707 year = 365 * month
708 decade = 10 * year
709 century = 10 * decade
710
711 time_units = [
712 nsec,
713 usec,
714 msec,
715 sec,
716 min,
717 hour,
718 day,
719 week,
720 month,
721 year,
722 decade,
723 century,
724 ]
725 time_labels = [
726 "1 ns",
727 "1 µs",
728 "1 ms",
729 "1 s",
730 "1 min",
731 "1 hour",
732 "1 day",
733 "1 week",
734 "1 month",
735 "1 year",
736 "1 decade",
737 "1 century",
738 ]
739
740 cutoff = (
741 next(
742 (i for i, x in enumerate(time_units) if x > max(xs)),
743 len(time_units) - 1,
744 )
745 + 1
746 )
747
748 plt.xticks(time_units[0:cutoff], time_labels[0:cutoff], rotation=90)
749 plt.legend(loc="upper left")
750 plt.show()
751
752 @property
753 def json(self):
754 """
755 Returns a JSON representation of the resource estimation result data.
756 """
757 if not hasattr(self, "_json"):
758 import json
759
760 self._json = json.dumps(self._data)
761
762 return self._json
763
764 def _summary_data_frame(self, **kwargs):
765 try:
766 import pandas as pd
767 except ImportError:
768 raise ImportError(
769 "Missing optional 'pandas' dependency. To install run: "
770 "pip install pandas"
771 )
772
773 # get labels or use default value, then extend with missing elements,
774 # and truncate extra elements
775 labels = kwargs.pop("labels", [])
776 labels.extend(range(len(labels), len(self)))
777 labels = labels[: len(self)]
778
779 def get_row(result):
780 if EstimatorResult._is_succeeded(result):
781 formatted = result["physicalCountsFormatted"]
782
783 return (
784 formatted["algorithmicLogicalQubits"],
785 formatted["logicalDepth"],
786 formatted["numTstates"],
787 result["logicalQubit"]["codeDistance"],
788 formatted["numTfactories"],
789 formatted["physicalQubitsForTfactoriesPercentage"],
790 formatted["physicalQubits"],
791 formatted["rqops"],
792 formatted["runtime"],
793 )
794 else:
795 return ["No solution found"] * 9
796
797 data = [get_row(self.data(index)) for index in range(len(self))]
798 columns = [
799 "Logical qubits",
800 "Logical depth",
801 "T states",
802 "Code distance",
803 "T factories",
804 "T factory fraction",
805 "Physical qubits",
806 "rQOPS",
807 "Physical runtime",
808 ]
809 return pd.DataFrame(data, columns=columns, index=labels)
810
811 def _item_result_table(self):
812 html = ""
813
814 if has_markdown:
815 md = markdown.Markdown(extensions=["mdx_math"])
816 for group in self["reportData"]["groups"]:
817 html += f"""
818 <details {"open" if group['alwaysVisible'] else ""}>
819 <summary style="display:list-item">
820 <strong>{group['title']}</strong>
821 </summary>
822 <table>"""
823 for entry in group["entries"]:
824 val = self
825 for key in entry["path"].split("/"):
826 if key not in val and "frontierEntries" in val:
827 val = val["frontierEntries"][0]
828 val = val[key]
829 if has_markdown:
830 explanation = md.convert(entry["explanation"])
831 else:
832 explanation = entry["explanation"]
833 html += f"""
834 <tr>
835 <td style="font-weight: bold; vertical-align: top; white-space: nowrap">{entry['label']}</td>
836 <td style="vertical-align: top; white-space: nowrap">{val}</td>
837 <td style="text-align: left">
838 <strong>{entry["description"]}</strong>
839 <hr style="margin-top: 2px; margin-bottom: 0px; border-top: solid 1px black" />
840 {explanation}
841 </td>
842 </tr>
843 """
844 html += "</table></details>"
845
846 html += f'<details><summary style="display:list-item"><strong>Assumptions</strong></summary><ul>'
847 if has_markdown:
848 for assumption in self["reportData"]["assumptions"]:
849 html += f"<li>{md.convert(assumption)}</li>"
850 html += "</ul></details>"
851
852 return html
853
854 def _item_result_summary_table(self):
855 html = """
856 <style>
857 .aqre-tooltip {
858 position: relative;
859 border-bottom: 1px dotted black;
860 }
861
862 .aqre-tooltip .aqre-tooltiptext {
863 font-weight: normal;
864 visibility: hidden;
865 width: 600px;
866 background-color: #e0e0e0;
867 color: black;
868 text-align: center;
869 border-radius: 6px;
870 padding: 5px 5px;
871 position: absolute;
872 z-index: 1;
873 top: 150%;
874 left: 50%;
875 margin-left: -200px;
876 border: solid 1px black;
877 }
878
879 .aqre-tooltip .aqre-tooltiptext::after {
880 content: "";
881 position: absolute;
882 bottom: 100%;
883 left: 50%;
884 margin-left: -5px;
885 border-width: 5px;
886 border-style: solid;
887 border-color: transparent transparent black transparent;
888 }
889
890 .aqre-tooltip:hover .aqre-tooltiptext {
891 visibility: visible;
892 }
893 </style>"""
894
895 if has_markdown:
896 md = markdown.Markdown(extensions=["mdx_math"])
897 for group in self["reportData"]["groups"]:
898 html += f"""
899 <details {"open" if group['alwaysVisible'] else ""}>
900 <summary style="display:list-item">
901 <strong>{group['title']}</strong>
902 </summary>
903 <table>"""
904 for entry in group["entries"]:
905 val = self
906 for key in entry["path"].split("/"):
907 val = val[key]
908 if has_markdown:
909 explanation = md.convert(entry["explanation"])
910 else:
911 explanation = entry["explanation"]
912 html += f"""
913 <tr class="aqre-tooltip">
914 <td style="font-weight: bold"><span class="aqre-tooltiptext">{explanation}</span>{entry['label']}</td>
915 <td>{val}</td>
916 <td style="text-align: left">{entry["description"]}</td>
917 </tr>
918 """
919 html += "</table></details>"
920
921 html += f"<details><summary style='display:list-item'><strong>Assumptions</strong></summary><ul>"
922 if has_markdown:
923 for assumption in self["reportData"]["assumptions"]:
924 html += f"<li>{md.convert(assumption)}</li>"
925 html += "</ul></details>"
926
927 return html
928
929 def _batch_result_table(self, indices):
930 succeeded_item_indices = [
931 i for i in indices if EstimatorResult._is_succeeded(self[i])
932 ]
933 if len(succeeded_item_indices) == 0:
934 print("None of the jobs succeeded")
935 return ""
936
937 first_succeeded_item_index = succeeded_item_indices[0]
938
939 html = ""
940
941 if has_markdown:
942 md = markdown.Markdown(extensions=["mdx_math"])
943
944 item_headers = "".join(f"<th>{i}</th>" for i in indices)
945
946 for group_index, group in enumerate(
947 self[first_succeeded_item_index]["reportData"]["groups"]
948 ):
949 html += f"""
950 <details {"open" if group['alwaysVisible'] else ""}>
951 <summary style="display:list-item">
952 <strong>{group['title']}</strong>
953 </summary>
954 <table>
955 <thead><tr><th>Item</th>{item_headers}</tr></thead>"""
956
957 visited_entries = set()
958
959 for entry in [
960 entry
961 for index in succeeded_item_indices
962 for entry in self[index]["reportData"]["groups"][group_index]["entries"]
963 ]:
964 label = entry["label"]
965 if label in visited_entries:
966 continue
967 visited_entries.add(label)
968
969 html += f"""
970 <tr>
971 <td style="font-weight: bold; vertical-align: top; white-space: nowrap">{label}</td>
972 """
973
974 for index in indices:
975 val = self[index]
976 if index in succeeded_item_indices:
977 for key in entry["path"].split("/"):
978 if key in val:
979 val = val[key]
980 else:
981 val = "N/A"
982 break
983 else:
984 val = "N/A"
985 html += f"""
986 <td style="vertical-align: top; white-space: nowrap">{val}</td>
987 """
988
989 html += """
990 </tr>
991 """
992 html += "</table></details>"
993
994 html += f'<details><summary style="display:list-item"><strong>Assumptions</strong></summary><ul>'
995 if has_markdown:
996 for assumption in self[0]["reportData"]["assumptions"]:
997 html += f"<li>{md.convert(assumption)}</li>"
998 html += "</ul></details>"
999
1000 return html
1001
1002 @staticmethod
1003 def _is_succeeded(obj):
1004 return "status" in obj and obj["status"] == "success"
1005
1006
1007class EstimatorResultDiagram:
1008 def __init__(self, data):
1009 data.pop("reportData")
1010 self.data_json = json.dumps(data).replace(" ", "")
1011 self.vis_lib = "https://cdn-aquavisualization-prod.azureedge.net/resource-estimation/index.js"
1012 self.space = HTMLWrapper(self._space_diagram())
1013 self.time = HTMLWrapper(self._time_diagram())
1014
1015 def _space_diagram(self):
1016 html = f"""
1017 <script src={self.vis_lib}></script>
1018 <re-space-diagram data={self.data_json}></re-space-diagram>"""
1019 return html
1020
1021 def _time_diagram(self):
1022 html = f"""
1023 <script src={self.vis_lib}></script>
1024 <re-time-diagram data={self.data_json}></re-time-diagram>"""
1025 return html
1026
1027
1028class LogicalCounts(dict):
1029 """
1030 Microsoft Resource Estimator Logical Counts.
1031
1032 The class represents logical counts that can be used as input to physical estimation of resources
1033 in the Microsoft Resource Estimator.
1034 """
1035
1036 def __init__(self, data: Dict):
1037 self._data = {}
1038 self._data["numQubits"] = data.get("numQubits", 0)
1039 self._data["tCount"] = data.get("tCount", 0)
1040 self._data["rotationCount"] = data.get("rotationCount", 0)
1041 self._data["rotationDepth"] = data.get("rotationDepth", 0)
1042 self._data["cczCount"] = data.get("cczCount", 0)
1043 self._data["ccixCount"] = data.get("ccixCount", 0)
1044 self._data["measurementCount"] = data.get("measurementCount", 0)
1045 super().__init__(self._data)
1046
1047 @property
1048 def json(self):
1049 """
1050 Returns a JSON representation of the logical counts.
1051 """
1052 if not hasattr(self, "_json"):
1053 import json
1054
1055 self._json = json.dumps(self._data)
1056
1057 return self._json
1058
1059 def estimate(
1060 self, params: Union[dict, List, EstimatorParams] = None
1061 ) -> EstimatorResult:
1062 """
1063 Estimates resources for the current logical counts, using the
1064 Parallel Synthesis Sequential Pauli Computation (PSSPC) layout method.
1065
1066 :param logical_counts: The logical counts.
1067 :param params: The parameters to configure physical estimation.
1068
1069 :returns resources: The estimated resources.
1070 """
1071 if params is None:
1072 params = [{}]
1073 elif isinstance(params, EstimatorParams):
1074 if params.has_items:
1075 params = params.as_dict()["items"]
1076 else:
1077 params = [params.as_dict()]
1078 elif isinstance(params, dict):
1079 params = [params]
1080 return EstimatorResult(
1081 json.loads(physical_estimates(self.json, json.dumps(params)))
1082 )
1083